AOJ : 2026 - Divisor is the Conqueror

問題概要

http://rose.u-aizu.ac.jp/onlinejudge/ProblemSet/description.jsp?id=2026
N 枚のカードがあります.
このカードは, トランプカードで, 1~13の数字が書かれたカードが, それぞれ最大 4 枚あります.
そのため, N の上限は 52 と設定されています.

このとき, 次のような条件を満たす並べ方があるならば, その並べ方を出力します.

  • 左側から数えて, i 番目のカードの数字は, 「1〜i-1 番目のカードの数字の合計」を割り切る.
  • N 枚のカードはそれぞれ 1 回ずつしか使用できない.

アルゴリズム

動的計画法でできないか思考錯誤していて, 結局できなかったため, shioshiota さんのブログを参考にさせていただきました.
http://d.hatena.ne.jp/shioshiota/20100812/1281624750

ありがとうございます!

ポイント

要素量が 52 の配列を作って, 再帰するごとにループしていると時間が間に合わない.
14 の大きさの配列に縮めて, 13 種類のカードのカウント値だけ持つ.
これによって, 大幅に計算量が削減される.

後ろから再帰しないと間に合わない理由は, いまいちわからない.
実際に書いてみて, 遅いことは確認しましたが.

プログラム

#define REP(i,n,m) for(int i=n;i<m;i++)
#define rep(i,n) REP(i,0,n)

int n,cnt[14],ans[54];

bool solve(int idx,int sum){
  if(idx == 0 && sum == 0) return true;
  if(idx < 0 || sum < 0) return false;

  rep(i,14){
    if(cnt[i]>0 && (sum - i) % i == 0){
      ans[idx-1] = i;
      cnt[i]--;
      if(solve(idx-1,sum-i)) return true;
      cnt[i]++;
    }
  }

  return false;
}

int main(void){
  while(cin>>n,n){
    int sum = 0;
    memset(cnt,0,sizeof(cnt));
    rep(i,n){
      int x;
      cin>>x;
      sum += x;
      cnt[x]++;
    }
    bool flg = solve(n,sum);
    if(flg){
      rep(i,n-1) cout<<ans[i]<<" ";
      cout<<ans[n-1]<<endl;
    }
    else{
      cout<<"No\n";
    }
  }

  return 0;
}