Facebook Hacker Cup 2013 Qualification Round

Problem A : Beautiful strings

出現回数が多いアルファベットに対して、大きい数字を割り当てればいいだけです。

int main(){
  int T;
  cin >> T;

  string s;
  getline(cin, s);

  for(int CASE = 1; CASE <= T; CASE++){
    cout << "Case #" << CASE << ": ";

    getline(cin, s);

    int cnt[26];
    memset(cnt, 0, sizeof(cnt));

    for(int i = 0; i < s.length(); i++){
      if(!isalpha(s[i])) continue;
      cnt[tolower(s[i]) - 'a']++;
    }

    int ans = 0;

    sort(cnt, cnt + 26, greater<int>());

    for(int i = 0; i < 26; i++){
      if(cnt[i] == 0) break;
      ans += cnt[i] * (26 - i);
    }

    cout << ans << endl;
  }
}

Problem B : Balanced Smileys

DPで解きました。
dp[何文字目][カッコを何個開いた状態か] = true or false (このパターンがあるかないか)
文字数がnとしたとき、dp[n][0]がtrueならば、閉じカッコにより開きカッコが全て閉じられて、カッコの対応が正しいということになるので、YESになります。

bool dp[102][102];

int main(){
  int T;
  cin >> T;

  string s;
  getline(cin, s);

  for(int CASE = 1; CASE <= T; CASE++){
    cout << "Case #" << CASE << ": ";

    getline(cin, s);

    memset(dp, 0, sizeof(dp));
    dp[0][0] = true;

    for(int i = 0; i < s.length(); i++){
      for(int j = 0; j <= s.length(); j++){
        if(!dp[i][j]) continue;

        bool col = (0 <= i - 1 && s[i - 1] == ':');

        if(s[i] == '('){
          dp[i + 1][j + 1] = true;
          if(col){
            dp[i + 1][j] = true;
          }
        }
        else if(s[i] == ')'){
          if(0 <= j - 1){
            dp[i + 1][j - 1] = true;
          }
          if(col){
            dp[i + 1][j] = true;
          }
        }
        else{
          dp[i + 1][j] = true;
        }
      }
    }

    cout << (dp[s.length()][0] ? "YES" : "NO") << endl;
  }
}

Problem C : Find the Min

「多分」、O(k)の解法

とりあえず、以下の性質は他の人も言ってますよね。

  • m[0]〜m[k-1]がどんな数列になるにしろ、m[1*k]〜m[2*k]には、0〜kの値がそれぞれ1個ずつ入る。
  • m[2*k+1]以降は、m[1*k]〜m[2*k]の周期が繰り返される。


2つ目の性質と、mapを利用して解いている人がいます( id:hirokazu1020さん : http://d.hatena.ne.jp/hirokazu1020/20130129/1359463036 )


僕の解法は、非常にこの人の解法と似ています。ただし、僕はmapではなく配列を使ってカウントしています。hirokazuさんの場合、m[0]~m[k-1]に出てくる、全ての数字の出現回数を数えていますが、実はカウントするのは0~kの数字の出現回数だけでいいです。これは、先ほど述べた1つ目の性質によるものです。いくらkより大きい数字をカウントしていたところで、周期の中にkより大きい数字が入ることはないわけだから、そんなカウントは必要ないわけです。

つまりこういう方針になります。

  • m[0]~m[k-1]に出現する、0以上k以下の値の出現回数を配列cntでカウントしておく(ただし、kより大きい数は無視する)。
    • 配列cntは、直前k個の数の出現回数を記憶した配列。
    • cnt[i]が0になっているときは、直前のk個の要素の中にiが含まれていないということになる。
  • 周期となるm[k]~m[k+k]の各要素を順番に求めていく。
  • 基本的に、0, 1, 2...の順番で小さい方から割り当てる。
  • ただし、添え字を後ろにずらしていく内に、使えるようになる数が出てくるので、その数の方が小さければ、そちらを使う。


以下のコードで、O(k)になってる(はず)。周期を求めるところがforとwhileの二重ループになっていて、一見O(k^2)に見えるかもですが、それは違います。外側のforループをk回まわしたとき、内側のwhileループがまわった回数を合計すると、それは最大でもk回にしかなりません。

typedef long long ll;

ll n, k;
ll a, b, c, r;
ll m[1000010];
ll cnt[1000010]; //カウント用配列

int main(){
  int T;
  cin >> T;

  for(int CASE = 1; CASE <= T; CASE++){
    cout << "Case #" << CASE << ": ";

    cin >> n >> k;
    cin >> a >> b >> c >> r;

    memset(cnt, 0, sizeof(cnt));
    m[0] = a;

    //0~kの数だけをカウントする
    if(m[0] <= k){
      cnt[m[0]] = 1;
    }

    for(int i = 1; i < k; i++){
      m[i] = (b * m[i - 1] + c) % r;

      //0~kの数だけをカウントする
      if(m[i] <= k){
        cnt[m[i]]++;
      }
    }

    //基本的に、このnowを周期に追加していく
    //1,2,3...の順番で小さい方を優先的に使うって言ってたやつです
    ll now = 0;

    vector<ll> cycle;

    //周期の要素である、k+1個の値を順番に求めます
    for(int i = k; i <= k + k; i++){
      //直前のk個より1個手前は、使われなくなる数字なので、これを取りだす
      ll val = (0 <= i - k - 1 ? m[i - k - 1] : INT_MAX);

      //0~kの値であれば、cntの配列を更新(直前のk個から1回分出現回数が減る)
      if(val <= k){
        cnt[val]--;
      }

      //小さい順に見て、次に使える数を探す
      //ここでnowを0からループさせていないのは、
      //[0]~[now-1]に使える数が存在しないから(valを除いて)
      while(cnt[now] > 0) now++;

      //新たに使えるようになる数(val)の方がnowより小さければ、それを使用
      if(val <= k && cnt[val] == 0 && val < now){
        cycle.push_back(val);
        cnt[val]++;
      }
      else{
        cycle.push_back(now);
        cnt[now]++;
      }
    }

    int idx = (n - k - 1) % cycle.size();
    cout << cycle[idx] << endl;
  }
}