AGC009E: Eternal Average

https://agc009.contest.atcoder.jp/tasks/agc009_e

まずK分木の葉に0や1を書き込んで、木の形状通りに平均を取っていく問題に言い換えます。

するとK分木は葉が全て0の部分を除けば、縦1直線に繋げばいいことがわかります。
証明のアイディアとしては、葉が全て1の木を一番深いところに生えている1が書き込まれた葉と入れ替える感じです。

こうすることで一番深いところに生えている葉はK枚、ほかの階層は(K-1)枚となりました。
あとはdp[i][j][k]:=(深さiまで見て、j個葉に1を書き込み、繰り上がりがk(0or1)の時の操作後の最終的な値としてあり得るものの場合の数)
の桁DPをしてO((N+M)^2)で解けます。

桁DPも多分するのは初めて?で結構戸惑いましたけど、背反であることを確認しながらやればそんな難しくはないですね。

ll dp[2][4010][2];
int N, M, K, T;

void solve() {
	cin >> N >> M >> K;
	int T = (N + M - 1) / (K - 1);

	int now = 0, nex = 1;
	dp[now][0][0] = 1;

	rep(i, 0, T) {
		memset(dp[nex], 0, sizeof(dp[nex]));
		rep(j, 0, M + 1) {
			rep(k, 0, 2) {
				if(!dp[now][j][k]) continue;
				if(j == 0) {
					assert(k == 0);
					rep(cnt, 0, K) {
						ADD(dp[nex][j + cnt][0], dp[now][j][k]);
					}
					ADD(dp[nex][j + K][1], dp[now][j][k]);
				}
				else {
					if(k == 0) {
						rep(cnt, 0, K) {
							ADD(dp[nex][j + cnt][0], dp[now][j][k]);
						}
					}
					else {
						rep(cnt, 0, K - 1) {
							ADD(dp[nex][j + cnt][0], dp[now][j][k]);
						}
						ADD(dp[nex][j + K - 1][1], dp[now][j][k]);
					}
				}
			}
		}

		swap(now, nex);
	}
	cout << dp[now][M][0] << "\n";
}

1600点まともに解けたのは初めてかな?

AtCoder Grand Contest 020D: Min Max Repetition

https://agc020.contest.atcoder.jp/tasks/agc020_d

お気持ちそのままが解法ですが、実装がめんどくさすぎる…。1発ACだけど詰めるのにすごい時間かかった。
A>Bとします。B<=Aの場合もだいたい同じです。

まず文字が何個まで連続できるかを求めます。これは少し考えればfloor( (A+B)/(B+1) )であることがわかります。これをUとおきましょう。

だいたい同じような文字列の繰り返しになってなるべくAを前に持ってくればいいんだろうなという感覚から考えて、
N,M,α,β,Lをそれぞれ0以上の自然数とすると、答えは
(A*U+B)*N+(A*α)+(B*β)+(A+B*U)*M+(A*L)となることが証明できます。
0<α<=Uの条件からNを二分探索で求め、M,α,β,Lも確定します。後は文字列を正しく出力すればいいです。

A,Bが大きいときどうだろうと考えられたのは良かったですね。極限状態を考えるとうまく行くことが多いのに最近気づきました。

int Q;
ll A, B, leftb, rightb;
ll U, N, M, alpha, beta, L;

void pre() {
	U = (A + B) / (B + 1);
	if(A == B) {
		N = B; M = 0; alpha = 0; beta = 0; L = 0;
		return;
	}
	if(A - U * B > 0) {
		N = B;
		M = 0;
		alpha = 0;
		beta = 0;
		L = A - U * B;
	}
	else {
		ll lv = 0, rv = (A + B) / (U + 1) + 10;
		while(rv - lv > 1) {
			ll m = (lv + rv) / 2;
			if(A - (U * m + (B - 1 - m) / U) > 0) lv = m;
			else rv = m;
		}
		N = lv;
		M = (B - 1 - N) / U;
		alpha = A - (U * N + M);
		beta = B - (N + U * M);
		L = 0;
	}
}

string F(ll lv, ll rv) {
	string res = "";
	for(ll i = lv; i < rv; i++) {
		if(i % (U + 1) == U) res += 'B';
		else res += 'A';
	}
	return res;
}

string G(ll lv, ll rv) {
	string res = "";
	for(ll i = lv; i < rv; i++) {
		if(i < alpha) res += 'A';
		else res += 'B';
	}
	return res;
}

string H(ll lv, ll rv) {
	string res = "";
	for(ll i = lv; i < rv; i++) {
		if(i % (U + 1) == 0) res += 'A';
		else res += 'B';
	}
	return res;
}

string I(ll lv, ll rv) {
	string res = "";
	for(ll i = lv; i < rv; i++) {
		res += 'A';
	}
	return res;
}

string pre2() {
	ll bd0 = 0; ll bd1 = (U + 1) * N; ll bd2 = (U + 1) * N + alpha + beta;
	ll bd3 = (A + B - L); ll bd4 = A + B;
	// debug(U, N, M, alpha, beta, L);
	// debug(bd0, bd1, bd2, bd3);
	string res = "";
	res += F(max(bd0, leftb) - bd0, min(bd1, rightb) - bd0);
	res += G(max(bd1, leftb) - bd1, min(bd2, rightb) - bd1);
	res += H(max(bd2, leftb) - bd2, min(bd3, rightb) - bd2);
	res += I(max(bd3, leftb) - bd3, min(bd4, rightb) - bd3);
	return res;
}

string sub_solve() {
	bool swaped = false;
	if(B > A) {
		swaped = true;
		swap(A, B);
		leftb = (A + B - 1) - leftb;
		rightb = (A + B - 1) - rightb;
		swap(leftb, rightb);
		rightb++;
	}
	else rightb++;
	pre();
	string res = pre2();
	if(swaped) {
		reverse(all(res));
		rep(i, 0, sz(res)) {
			if(res[i] == 'A') res[i] = 'B';
			else res[i] = 'A';
		}
	}
	return res;
}

void solve() {
	cin >> Q;
	while(Q--) {
		cin >> A >> B >> leftb >> rightb; leftb--; rightb--;
		cout << sub_solve() << "\n";
	}
}

AGC018D: Tree and Hamilton Path

https://agc018.contest.atcoder.jp/tasks/agc018_d

アイディア自体はそこまで難しくないけど、証明がややこしいやつ。

重心をとって、各子の部分木を渡り歩けば良いのかなぁという予想は簡単につくので、それが最善であることを頑張って示せばいいです。
ポイントは重心周りの辺全てを最善で取ることができないことと、ある部分木の大きさは他の部分木の大きさの合計以下になることです。

あとは重心が1つor2つの時を正しく捌けば大丈夫です。

int N;
vector<pl> G[MAX_N];
vi cen; //one or two
ll ans = 0;

int search_centroid(int v, int p) {
	int s = 1, m = 0;
	rep(i, 0, sz(G[v])) {
		int n = G[v][i].fst;
		if(p == n) continue;
		int t = search_centroid(n, v);
		s += t;
		MAX(m, t);
	}
	MAX(m, N - s);
	if(m <= N / 2) {
		cen.pb(v);
	}
	return s;
}

int loop(int v, int p, ll c) {
	int tsize = 1;
	rep(i, 0, sz(G[v])) {
		int n = G[v][i].fst;
		ll nc = G[v][i].sec;
		if(p == n) continue;
		tsize += loop(n, v, nc);
	}
	ans += tsize * c * 2;
	return tsize;
}

void solve() {
	cin >> N;
	rep(i, 0, N - 1) {
		int a, b; ll c;
		cin >> a >> b >> c; a--; b--;
		G[a].pb(pl(b, c));
		G[b].pb(pl(a, c));
	}
	search_centroid(0, -1);
	if(sz(cen) == 2) {
		int c1 = cen[0], c2 = cen[1];
		loop(c1, c2, 0);
		loop(c2, c1, 0);
		ll cost;
		rep(i, 0, sz(G[c1])) {
			if(G[c1][i].fst != c2) continue;
			cost = G[c1][i].sec;
		}
		ans += (N - 1) * cost;
	}
	else {
		int c = cen[0];
		loop(c, -1, 0);
		ll cost = inf;
		rep(i, 0, sz(G[c])) {
			MIN(cost, G[c][i].sec);
		}
		ans -= cost;
	}
	cout << ans << "\n";
}

AtCoder Grand Contest 009C: Division into Two

https://agc009.contest.atcoder.jp/tasks/agc009_c

実家やるだけのはずなんですが通らないので後回しにします…。

int N; ll A, B;
BIT bitx, bity;
ll D[MAX_N];

const ll UB = 1e18;

int cd(ll a) {
	return lower_bound(D, D + N, a) - D;
}

void solve() {
	cin >> N >> A >> B;
	D[0] = -4 * (UB + 1); D[1] = -2 * (UB + 1);
	rep(i, 0, N) cin >> D[i + 2];


	N += 2;
	bitx.init(N);
	bity.init(N);

	bitx.update(0, 1, 1);


	rep(i, 2, N) {
		int xat = cd(D[i] - (B - 1)), yat = cd(D[i] - (A - 1));
		ll vx = bity.get(boundy, yat), vy = bitx.get(boundx, xat);

		if(D[i] - D[i - 1] < A) boundx = i - 1;
		if(D[i] - D[i - 1] < B) boundy = i - 1;

		bitx.update(i - 1, i, vx); bity.update(i - 1, i, vy);
		// debug(i + 1, "x", bitx.show('x'), "y", bity.show('y'), boundx, boundy);
	}
	ll res = bitx.get(boundx, N) + bity.get(boundy, N);
	res %= mod;
	cout << res << "\n";
}

AGC003E: Anticube

https://agc003.contest.atcoder.jp/tasks/agc003_d

本質を勘違いした…

まず素数のうちa個あるものはa%3個にしてよいです。それでvをcubeにする値uをつなげたグラフを考えると二部グラフになるので、max( (vの数),(uの数) )の合計を求める問題に帰着できます。

しかし、vの素因数分解を愚直にやるとO(sqrt(s)N)になって間に合いません。
なので次のように工夫するとうまく行きます。

  • 3000までの素数は愚直に求めて、3000までの素数を取り除いた状態の数をaとする。aが平方数ならsqrt(a)を要素に加えて、そうでなければ素数とみなす。

証明
3000^3>=10^10なのでaには3000以上の素数が3つ以上含まれることはありません。ということで、aは(1)素数が1つ,(2)異なる素数が2つ(3)同じ素数のいずれかに素因数分解されることがわかります。
(1)の場合は問題ありません。
(2)の場合もaとペアになるのはa^2(>=3000^4>=10^10)となるので素数とみなしてもペアが存在せず問題ないです。
(3)の場合もaが平方数か考えるので問題ないです。

これで3000というのは大体O(s^(1/3))なのでO(s^(1/3)N)となりギリギリ間に合います。

普通にデータ構造パートでTLEしているのかと思っていろいろ無駄なことしてしまった…。

void update(pl& p, ll a, int cnt) {
	if(cnt == 1) {
		p.fst *= a;
		p.sec *= a * a;
	}
	else {
		p.fst *= a * a;
		p.sec *= a;
	}
}

bool ok(ll a, ll b) {
	int cnt = 0;
	while(a >= 10) {
		a /= 10; cnt++;
	}
	while(b >= 10) {
		b /= 10; cnt++;
	}
	return cnt <= 10;
}

pl ntn(ll a) {
	map<ll, int> res;
	for(ll i = 2; i <= 3000; i++) {
		while(a % i == 0) {
			res[-i]++;
			a /= i;
		}
	}
	ll sa = ll(sqrt(a) + eps);
	if(a != 1) {
		if(a == sa * sa) res[-sa] += 2;
		else res[-a]++;
	}
	// debug(vector<pl>(all(res)));

	pl pre1 = pl(1, 1);
	pl pre2 = pl(1, 1);
	bool fst = true;
	for(auto p : res) {
		p.sec %= 3;
		if(p.sec == 0) continue;
		ll v = -p.fst;
		if(fst) {
			update(pre1, v, p.sec);
			fst = false;
		}
		else update(pre2, v, p.sec);
	}
	// debug(pre1, pre2);
	if(ok(pre1.sec, pre2.sec)) {
		return pl(pre1.fst * pre2.fst, pre1.sec * pre2.sec);
	}
	else return pl(pre1.fst * pre2.fst, -1);
}

int N;
map<pl, int> G;

void solve() {
	cin >> N;
	rep(i, 0, N) {
		ll a; cin >> a;
		G[ntn(a)]++;
	}

	int cnt = 0;
	for(auto pp : G) {
		// debug(pp);
		pl pt = pp.fst;
		int v = pp.sec;
		if(pt == pl(1, 1)) cnt += 2;
		else if(pt.sec == -1) cnt += v * 2;
		else {
			swap(pt.fst, pt.sec);
			if(G.find(pt) != G.end()) {
				int w = G[pt];
				cnt += max(v, w);
			}
			else cnt += v * 2;
		}
	}
	cout << cnt / 2 << "\n";
}

なんか3000じゃなくて400でも通ったんだけどどうしてだろう…

AtCoder Petrozavodsk Contest 001F: XOR Tree

https://apc001.contest.atcoder.jp/tasks/apc001_f


最近同じような問題を解いたのでわかったんですけども、O(3^N)DPをバグらせて悲しくなりました…。

頂点に隣接しているすべての辺のXORを書き込みます。すると、2つの頂点についてある値XでのXORを行い、すべての頂点を0にする問題になりました。そうすれば、頂点の値がiのものの数をciとして、i=0は考えなくて良くて、ci(i>=1)は[ci/2]個のペアを作るのが最適です。あと余ったものでO(3^N)DPをすればいいです。

XORを区間に施す際の一般的なテクニックについてですが、2項の差というよりも、2つの値のXORと考えたほうが良さそうですね。

bitとbit2を混同してWA出したのは情けない…。for(int j = (i - 1) & i; j ; j = (j -1)&i))みたいな書き方を速くマスターしましょう…。

これで1000点埋め完了です。

(3/4更新)

rep(bit, 0, (1 << N)) {
		for(int to = bit; to; to = (to - 1) & bit) {
			MAX(dp[bit], dp[to ^ bit] + ok[to]);
		}
	}
}

このようにしてもできます。vector使わず簡潔でいいですね。
bit^toがbitの部分集合(bitを含まない)となります。

int A[MAX_N];
int cnt[20];

int dp[(1 << 16)];
int ok[(1 << 16)];
vector<int> vec;

void solve() {
	cin >> N;
	rep(i, 0, N - 1) {
		int a, b, c;
		cin >> a >> b >> c;
		A[a] ^= c;
		A[b] ^= c;
	}
	rep(i, 0, N) cnt[A[i]]++;

	int ans = 0;
	rep(i, 1, 16) {
		ans += cnt[i] / 2;
		if(cnt[i] % 2) vec.pb(i);
	}

	M = sz(vec);

	rep(bit, 1, (1 << M)) {
		int tmp = 0, res = 0;
		rep(i, 0, M) {
			if(!(bit & (1 << i))) continue;
			tmp ^= vec[i];
			res++;
		}
		if(tmp == 0) ok[bit] = res - 1;
		else ok[bit] = M;
	}
	fill(dp, dp + (1 << M), M);
	dp[0] = 0;
	rep(bit, 0, (1 << M)) {
		vector<int> vt;
		rep(i, 0, M) {
			if(!(bit & (1 << i))) vt.pb(i);
		}
		rep(bit2, 1, (1 << sz(vt))) {
			int to = 0;
			rep(i, 0, sz(vt)) {
				if(bit2 & (1 << i)) to |= (1 << vt[i]);
			}
			MIN(dp[bit | to], dp[bit] + ok[to]);
		}
	}
	cout << ans + dp[(1 << M) - 1] << "\n";
}

「みんなのプロコン」D: 工場

https://yahoo-procon2017-qual.contest.atcoder.jp/assignmentshttps://yahoo-procon2017-qual.contest.atcoder.jp/tasks/yahoo_procon2017_qual_d

i日目の最適な売り方をした時の商品の残りをBi個としてグラフを考えます。(D,A)はグラフをD日からAだけ下げて、マイナスの値を0になるように調節するような作業になります。経理質問DはD日まで売った数を単に答えればいいです。

ここで一度0になった日にちはそれ以降0以外の値を取らないことに注目します。
するとD日より後で初めて0を取る日にちをLDとすれば、[D, LD)の区間についてAだけ下げればいいです。
そしてマイナスの値を0に調節するんですが、これは各日にちで1回しかしなくて良いです。なので座標圧縮すればO(QlogQ)で全クエリを捌けます。

よくある1回しか使わないの条件ですね。

int Q; ll K;
int N;
ll C[MAX_N], D[MAX_N], A[MAX_N];
vector<ll> vec;
segtree prod; BIT sale;
set<int> zero;

void bit_show(BIT& bit) {
	vector<ll> vec;
	rep(i, 0, bit.n) vec.pb(bit.get(i, i + 1));
	debug(vec);
}

int cd(ll a) {
	return lower_bound(all(vec), a) - vec.begin();
}

void solve() {
	cin >> Q >> K;
	rep(i, 0, Q) {
		cin >> C[i];
		if(C[i] == 1) cin >> D[i] >> A[i];
		else cin >> D[i];
		vec.pb(D[i]);
	}
	sort(all(vec));
	vec.erase(unique(all(vec)), vec.end());

	N = sz(vec);

	prod.init(N);
	sale.init(N + 1);
	zero.insert(N);

	rep(i, 0, N) {
		ll d = vec[i];
		prod.update(i, i + 1, d * K);
	}

	rep(i, 0, Q) {
		if(C[i] == 1) {
			int from = cd(D[i]);
			int to = *zero.lower_bound(from);
			prod.update(from, to, -A[i]);
			sale.update(from, from + 1, A[i]);
			sale.update(to, to + 1, -A[i]);

			while(true) {
				pl p = prod.get(from, to);
				ll v = p.fst, id = p.sec;
				int temto = *zero.lower_bound(id);
				// debug("omo", v, id);
				// prod.show();
				if(v >= 0) break;
				prod.update(id, temto, -v);
				sale.update(id, id + 1, v);
				sale.update(temto, temto + 1, -v);
				zero.insert(id);
			}
		}
		else {
			int did = cd(D[i]);
			cout << sale.get(0, did + 1) << "\n";
		}
		// prod.show();
		// bit_show(sale);
		// debug(vi(all(zero)));
	}
}

Editorialの解法も見とかないと。