pekempeyのブログ

競技プログラミングに関する話題を書いていきます。

Educational Codeforces Round 25: F. String Compression

http://codeforces.com/contest/825/problem/F

解法

文字列の最小周期はKMP法のテーブルを用いて計算できる。kmp[i]は文字列 S の先頭 i 文字を切り出した文字列の prefix と suffix の最大共通部分である。たとえば aabaabaab という文字列ならば 6 である。

aabaabaab
******
   ******

もし文字列に周期があるならば、i-kmp[i]が最小の周期になる。これはよく考えると分かるだろう。逆に文字列に周期がないならば、iはi-kmp[i]の倍数にはならない。

KMPテーブルの計算はO(n)でできるため、全体でO(n^2)で解くことができた。

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

int main() {
    std::string s;
    std::cin >> s;

    const int n = s.size();

    std::vector<int> len(n + 1);
    for (int i = 1; i <= n; i++) {
        len[i] = len[i / 10] + 1;
    }

    std::vector<int> dp(n + 1, 1e9);
    std::vector<int> kmp(n + 1);
    dp[0] = 0;
    for (int i = 0; i < n; i++) {
        kmp[i] = -1;
        int u = -1;
        for (int j = i + 1; j <= n; j++) {
            while (u != -1 && s[j - 1] != s[i + u]) u = kmp[i + u];
            if (u == -1) {
                kmp[j] = u = 0;
            } else {
                kmp[j] = ++u;
            }
            int period = (j - i) - kmp[j];
            if ((j - i) % period != 0) {
                period = j - i;
            }
            dp[j] = std::min(dp[j], dp[i] + len[(j - i) / period] + period);
        }
    }

    std::cout << dp[n] << std::endl;
}

最小周期が求められることの証明

周期が存在しないときは自明である。周期が存在するとき正しく求められるのかを考察する。

周期が存在するとき、たとえば以下のような対が考えられる。

f:id:pekempey:20170718002559p:plain:w500

文字列の長さを \(N\)、最小周期を \(T\)、kmpの値を \(K\) としたとき \(K>=N-T\) は自明に成り立つ。しかし \(K \gt N - T\) となってしまうと周期が正しく取れなくなってしまう。実はそうはならないことを示せば良い。

f:id:pekempey:20170718002928p:plain:w500

もし \(N-T\)よりも \(K\) が大きくなったとする。等しいノード同士を結ぶと以下のようになる。よく見るとすべてのノードが連結である。何故連結になるのかを示す。

f:id:pekempey:20170718003011p:plain:w500

重要な考察として、最小周期は素数である。\(N-K \lt T\) なので \(N-K\) は \(T\) の倍数ではありえない。つまり \(N-K\) と \(T\) は互いに素である。

f:id:pekempey:20170718003143p:plain:w500

互いに素なので、整数論のよくある定理(拡張ユークリッド互除法とかで見るやつ)により任意の地点に到達できる。よってすべての値が等しいということになる。しかしこれは矛盾、つまり仮定が誤っている。

Educational Codeforces Round 25: G. Tree Queries

http://codeforces.com/contest/825/problem/G

解法

f:id:pekempey:20170717205714p:plain

一番最初の黒頂点を根 r として考える。黒頂点に囲まれている部分を black-tree と呼ぶことにする。

タイプ1
u-r パスを black-tree に併合すればいい。事前に r-u パス上の最小頂点を求めておくことで O(1) で併合と同等の操作が行える。

タイプ2
white-path と black-tree の最小値を求めれば良い。white-path を厳密に求める必要はなく、r-u パス上の最小頂点のみを求めれば良い。これも O(1) でできる。

#include <iostream>
#include <algorithm>
#include <vector>

int input() {
    int n;
    scanf("%d", &n);
    return n;
}

int main() {
    int n, q;
    std::cin >> n >> q;

    std::vector<std::vector<int>> g(n);
    for (int i = 0; i < n - 1; i++) {
        int u = input() - 1;
        int v = input() - 1;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    input();
    int root = input() % n;
    q--;

    std::vector<int> white_path(n, -1);
    std::vector<int> stack;
    white_path[root] = root;

    stack.push_back(root);
    while (!stack.empty()) {
        const int u = stack.back();
        stack.pop_back();
        white_path[u] = std::min(u, white_path[u]);

        for (int v : g[u]) {
            if (white_path[v] == -1) {
                white_path[v] = white_path[u];
                stack.push_back(v);
            }
        }
    }

    int ans = 0;
    int black_min = 1e9;
    while (q--) {
        int t = input();
        int x = (input() + ans) % n;

        if (t == 1) {
            black_min = std::min(black_min, white_path[x]);
        } else {
            ans = std::min(white_path[x], black_min) + 1;
            printf("%d\n", ans);
        }
    }
}

prime counting function

以下の問題を解く際に「n 以下の素数の個数」を高速に求める必要が出てくる。

http://codeforces.com/contest/665/problem/F

素数列を\(p_0,p_1,\ldots\)とする。

\(\phi(x,p_i)\)を\(x\)以下の正整数のうち\(p_0,p_1,\ldots,p_i\)を約数に持たないものの個数と定義する。このとき以下の漸化式が成り立つ。

\[ \phi(x,p_i)=\phi(x,p_{i-1})-\phi\left(\frac{x}{p_i},p_{i-1}\right) \]

さらに\(\pi(x,p_i)=\phi(x,p_i)+i+1\)と定義する。\(x \le p_i^2\)のとき\(\pi(x,p_i)=\pi(x)\)になることは試し割りの素数判定の原理を考えれば容易にわかる。2,3,5,7,...で割り切れない正整数の個数を数えているのであまり良く考えないと 1 を素数に含めてしまうので注意

これを最初の漸化式に当てはめると以下の漸化式が導ける。

\[ \pi(x,p_i)=\pi(x,p_{i-1})-\pi\left(\frac{x}{p_i},p_{i-1}\right)+i+1 \]

\( \pi(x,p_i) \) のテーブルを更新するとき、\( \pi(1..p_i^2-1,p_i) \) の値は変わらないことを使うと、DPテーブルの更新は大幅に高速化できる。

この手法を使うとpi(N/i)側が求められるという利点がある。1000000以下の素数で計算しても1sec程度である。

#include <iostream>
#include <ctime>
#include <vector>
#include <algorithm>

using namespace std;

const long long N = 5e5;
bool tab[N + 1];
long long pi[N + 1];
long long hi[N + 1];
vector<long long> primes;

int main() {
	long long n;
	cin >> n;
	// 1 is prime !!
	for (int i = 1; i <= N; i++) {
		pi[i] = i;
		hi[i] = n / i;
		tab[i] = true;
	}
	for (int i = 2; i <= N; i++) {
		if (tab[i]) {
			primes.push_back(i);
			for (int j = i * 2; j <= N; j += i) {
				tab[j] = false;
			}
		}
	}
	for (int i = 0; i < primes.size(); i++) {
		long long p = primes[i];
		// n/j>=p*p
		// j<=n/(p*p)
		for (int j = 1; j <= min(N, n / (p * p)); j++) {
			long long k = n / j / p;
			hi[j] -= (k <= N ? pi[k] : hi[j * p]) - (i + 1);
		}
		for (int j = N; j >= p * p; j--) {
			pi[j] -= pi[j / p] - (i + 1);
		}
	}
	long long ans = 0;
	for (long long p : primes) {
		if (p * p * p <= n) {
			ans++;
		}
		ans += max(0LL, hi[p] - pi[p]);
	}
	cout << ans << endl;
}