けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

AOJ 2441 FizzBuzz (JAG 夏合宿 2012 day3b-B) (450 点)

AOJ-ICPC で無作為に問題を選んで実装する練習をした。あと、桁 DP でやってしまったけど、もっとずっとはるかに楽な方法があった。。。

問題へのリンク

問題概要

1 から順に FizzBuzz をやって得られる文字列を FizzBuzz 文字列と呼ぶ。

12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz1617...

と続いていく。整数 s が与えれれて、この文字列の s 番目 (1-indexed) から 20 文字分を出力せよ。

制約

  • 1 <= s <= 1018

解法

「k 番目を求めよ」
⬇︎
「x 以下の中に k 個あるかどうかを判定する問題にして二分探索」

というのは超典型ではある。この問題もそのフレームワークにガッツリ当てはまる。というわけで、

「1 から x までについて FizzBuzz 文字列を出したときにそれが何文字あるか?」

を求める問題に落ちる。それが解ければ二分探索することで、s 番目が何を出力しようとしていたかがわかる。これを解くのは想定解法は色々頑張るっぽい。自分は桁 DP でやった。

dp[(どの桁まで見たか)][leading zero が何個か][smaller][15 で割った余り] := 個数

ちょっと大変だが、このテーブルを桁 DP によって求めれば、1〜x までの FizzBuzz 文字列が何文字あるのかを計算することができる。

#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <sstream>
using namespace std;

// calc digit-dp
// dp[(桁)][(num of leading-zero)][smaller][15 で割ったあまり] := 個数
long long dp[21][21][3][16];
long long calc(long long num) {
  memset(dp, 0, sizeof(dp));
  dp[0][0][0][0] = 1;
  stringstream ss; ss << num;
  string str = ss.str(); ss >> str;
  for (int i = 0; i < str.size(); ++i) {
    int cur = (int)(str[i] - '0');
    for (int mod = 0; mod < 15; ++mod) {
      // (leading zero = i, exact(0)) ->
      if (cur == 0) dp[i+1][i+1][0][mod*10%15] += dp[i][i][0][mod];
      else dp[i+1][i+1][1][mod*10%15] += dp[i][i][0][mod];
      if (cur != 0) dp[i+1][i][0][(mod*10+cur)%15] += dp[i][i][0][mod];
      for (int c = 1; c < cur; ++c) {
        dp[i+1][i][1][(mod*10+c)%15] += dp[i][i][0][mod];
      }

      // (leading zero = i, smaller(1)) ->
      dp[i+1][i+1][1][mod*10%15] += dp[i][i][1][mod];
      for (int c = 1; c < 10; ++c) {
        dp[i+1][i][1][(mod*10+c)%15] += dp[i][i][1][mod];
      }

      // (leading zero < i, exact(0)) ->
      for (int j = 0; j < i; ++j) {
        dp[i+1][j][0][(mod*10+cur)%15] += dp[i][j][0][mod];
        for (int c = 0; c < cur; ++c) {
          dp[i+1][j][1][(mod*10+c)%15] += dp[i][j][0][mod];
        }
      }

      // (leading zero < i, smaller(1)) ->
      for (int j = 0; j < i; ++j) {
        for (int c = 0; c < 10; ++c) {
          dp[i+1][j][1][(mod*10+c)%15] += dp[i][j][1][mod];
        }
      }
    }
  }

  // num
  long long res = 0;
  for (int j = 0; j < str.size(); ++j) {
    for (int mod = 0; mod < 15; ++mod) {
      if (mod % 3 == 0) continue;
      if (mod % 5 == 0) continue;
      long long num = dp[str.size()][j][0][mod] + dp[str.size()][j][1][mod];
      res += num * ((long long)str.size() - j);
    }
  }

  // fizzbuzz
  long long fizzbuzz = (num/3) * 4 + (num/5) * 4;

  return res + fizzbuzz;
}

int main() {
  long long s; cin >> s;
  
  // 1 から x までの文字列の総数が s 未満となる最大の x を求める
  long long low = 0, high = 1LL<<58;
  while (high - low > 1) {
    long long mid = (high + low) / 2;
    long long all = calc(mid);
    if (all >= s) high = mid;
    else low = mid;
  }

  long long rem = s - calc(low) - 1;
  stringstream ss;
  for (long long i = high; i < high + 20; ++i) {
    if (i % 15 == 0) ss << "FizzBuzz";
    else if (i % 3 == 0) ss << "Fizz";
    else if (i % 5 == 0) ss << "Buzz";
    else ss << i;
  }
  string res = ss.str();
  cout << res.substr(rem, 20) << endl;
}