不定期プログラミング覚え書き

青コーダーと黄コーダーの間を彷徨う社会人プロコン勢が余力のあるときに復習した内容をまとめるブログ

Google Code Jam Round1C C: Fashion Police

問題

https://code.google.com/codejam/contest/4314486/dashboard#s=p1
J・P・S個( J\leq P \leq S)ずつのジャケットとズボンとシャツを持っている人がいる。
彼の住んでいる世の中は恐ろしい所で、同じ服装をすると逮捕されるらしい(雑な理解)
更に、世知辛い世の中なので、ジャケット・ズボン・シャツのうちの2つが同じ服装もK回までしか許されない。
最大何日この人は警察に捕まらず生きていけるかを答えよ。またその時の服装も出力せよ

考えたこと(Small)

SmallはSが3以下らしいので、
3*3*3の最大27通りの服装から、採用する服装の集合2^27を列挙してみて、題意を満たすかどうかのチェックをしてあげると良い。
ケース数が100あるので、最大計算時間は100*2^{27} \approx 1.3*10^{10}だが…間に合うかな?
とおもいつつ、半信半疑で実装。
実際自分のマシン環境で2分位かけたら答えを返してくれたのでセーフだった。
(但しfirst tryでは出力値を0-indexingにしてしまうと言う痛恨のミスにより時間切れ)

コード(Small)
int bitCount( int mask ){
   int cnt=0;
   while( mask != 0 ){
      cnt++;
      mask &= mask-1;
   }
   return cnt;
}

int main(){
  int n_case;
  cin >> n_case;
  for( int loop = 0 ; loop < n_case ; loop++ ){
   int J,P,S,K;
   cin >> J >> P >> S >> K;
   vector<pair<int,pair<int,int> > > c;
   for( int i =0 ; i < J; i++ ){
      for( int j = 0 ; j <P; j++ ){
         for( int k = 0 ; k < S; k++ ){
            c.push_back( make_pair(i, make_pair(j, k)) );
         }
      }
   }
   int size = c.size();
   int ansMask = 0;
   for( int mask=0; mask < (1<<size); mask++){
      if( bitCount(ansMask) > bitCount( mask ) ) continue;
      bool ok = true;
      map<int,int> ab;
      map<int,int> bc;
      map<int,int> ac;
      for( int i = 0 ; i < size; i++){
         if( mask&(1<<i)){
            int A=c[i].first;
            int B=c[i].second.first;
            int C=c[i].second.second;
            ab[A*10+B]++;
            bc[B*10+C]++;
            ac[A*10+C]++;
            if( ab[A*10+B] > K || bc[B*10+C]> K || ac[A*10+C] > K ){
               ok=false;
               break;
            }
         }
      }
      if( ok ) ansMask=mask;
   }
   cout << "Case #" << loop+1 << ": " <<bitCount(ansMask)<< endl;
   for( int i = 0 ; i < size; i++ ){
      if( ansMask&(1<<i) ){
            int A=c[i].first;
            int B=c[i].second.first;
            int C=c[i].second.second;
            cout <<A+1 <<" " << B+1 <<" " << C+1 << endl;
      }
   }
  }
  return 0;
}
考えたこと(Large)

さて34 pointのlargeである。
とりあえず、J,P,Sの大小関係が決まっているので、小さい方をまず固定して考えよう。
j,pの各組み合わせをK回ずつ使う(K>Sの場合はK=Sとみなすとする)として、sの割当を題意を満たすように上手く調整できないかと考える。
直感的に、
(j=1,p=1)の時にあわせるsは1~K,
(j=1,p=2)の時に合わせるsは2~K+1,
(j=1,p=3)の時に合わせるsは3~K+2,
(中略)
(j=2,p=1)の時に合わせるsは2~K+1,
(j=2,p=2)の時に合わせるsは3~K+2,

という風にj,pの値に従いsの使用開始位置をずらすようなコードを書いてみた。 (もちろんsの値がSより大きくなった場合はmodをとって調整する)

どうもうまい具合に重複せずに出力できているようである。
時間も迫っているのでこの実装で題意を満たさない時にエラーを吐くようなエラーコード付きでトライ。
エラーも吐かなかったので提出してみた。結果通った。正直通るとは思っていなかったのでびっくりした。

コード(Large)
int main(){
  int n_case;
  cin >> n_case;
  for( int loop = 0 ; loop < n_case ; loop++ ){
   int J,P,S,K;
   cin >> J >> P >> S >> K;
   cout << "Case #" << loop+1 << ": " <<J*P*min(S,K)<< endl;
   map<int,int> ab;
   map<int,int> bc;
   map<int,int> ca;
   for( int j=1; j<=J; j++){
      for( int p=1; p<=P; p++ ){
         for( int s=1; s<=min(K,S); s++ ){
            int a=j;
            int b=p;
            int c=(s+p+j-3)%S+1;
            cout << a << " " << b << " " << c << endl;
            ab[a*100+b]++;
            bc[b*100+c]++;
            ca[c*100+a]++;
            if(ab[a*100+b]>K || bc[b*100+c]>K || ca[c*100+a]>K ){
               cerr<< "ERROR in case "<<loop+1<<endl;
               return 0;
            }
         }
      }
   }
  }
  return 0;
}
考察

…で、なんでこれで通るのよ?と言う話について、
jを1として固定した時について改めて確認する (pを1と固定した場合でも同様に確認できるし、固定する値が1でなくとも同様に確認できる)

SがKより小さい場合、
(j=1,p=1)の時にあわせるsは1~S,
(j=1,p=2)の時に合わせるsは2~S,1
(j=1,p=3)の時に合わせるsは3~S,1,2
(中略)
(j=1,p=P)の時に合わせるsはP~S,1,2,...,P-1
...と、結局j=1のジャケットに対して1~Sの各シャツがP回ずつ使われることになる。
しかし、P\leq S\le Kであるから、この割当は題意を満たす。

SがK以上の場合、
case 1: (j=1,p=1)の時にあわせるsは1~K,
case 2: (j=1,p=2)の時に合わせるsは2~K+1
case 3: (j=1,p=3)の時に合わせるsは3~K+2
(中略)
case K: (j=1,p=K)の時に合わせるsはK~2K,
(中略)
case P: (j=1,p=P)の時に合わせるsはP~K+P
シャツKはcase 1~case Kで必ず利用されていることから、
この割り当て方でK+1回利用されているシャツが存在すると仮定するならば、シャツKが真っ先にK+1回使われているはずである。
ところで、case K+1以降でKが利用されるためには、一周回ってKに到達する必要がある(上手く表現できていない気がするが…)から、
少なくともcase P でのsの割当の最大値であるK+PはS+Kより大きく無くてはならない。
しかし、題意によりP<=Sであるから、これは矛盾である。
よって背理法により、K+1回利用されているシャツは存在しない。

というわけで、直感ではあったけれど、1ずつずらす方法によって上手くK+1回使用される組合せを排除できていることがわかる。