Codeforces Round #399 (Div.1 + Div.2, combined)

这场比赛又只做出前两题......事后结合官方的教程学习了一番. # A. Oath of the Night's Watch 给一个长度为n的数列, 问多少个数满足至少有一个严格小于它, 同时至少有一个严格大于它.

7分钟之后才通过此题, 也许是理解题意花了些时间吧......唉, 慢就慢点吧.

#include <iostream>
#include <algorithm>
using namespace std;
const int MAX_N = 1e5;
int a[MAX_N];
int main()
{
	int n, mn, mx, ans = 0;
	cin >> n >> a[0];
	mn = mx = a[0];
	for (int i = 1; i < n; ++i) {
		cin >> a[i];
		mn = min(mn, a[i]);
		mx = max(mx, a[i]);
	}
	for (int i = 0; i < n; ++i)
		ans += a[i] > mn && a[i] < mx;
	cout << ans << endl;
	return 0;
}

B. Code For 1

按照这种方式展开一个序列: 将大于1的数x用, , 三个数代替, 直到不能再操作. 给出长度为1的初始序列[n], 问完全展开后, 位置中有多少个1 (1-indexed). (0≤n<2^50, 0≤r-l≤10^5, r≤1, l≤1)

  1. 为[n]展开后1的个数, 则, 也许会觉得这个式子很熟悉, 因为这就是从低位到高位读取二进制数的方法, 它的解是.

  2. 同时, 我们也发现, [n]完全展开后的长度为大于它的最小的2的幂减去1, 因为递推式满足, 还是在读二进制数, 只是把每一位都替换成了1.

所以可以类比在二叉树上求rank的过程, 求两个前缀和, 作差即得答案.

那么l, r为什么给的这么小呢? 官方教程中的解法是的, 仅用到了性质 2), 对每一位分治一次.

#include <iostream>
using namespace std;
typedef long long ll;

ll cal(ll x, ll sz, ll k)
{
	if (x <= 1)
		return x;
	ll y = x/2, s = sz/2;
	if (k < s)
		return cal(y, s, k);
	if (k == s)
		return y;
	if (k == s+1)
		return y + (x&1);
	return y + (x&1) + cal(y, s, k-s-1);
}

int main()
{
	ll n, l, r, s = 1;
	cin >> n >> l >> r;
	while (s < n)
		s = (s << 1) | 1;
	cout << cal(n, s, r) - (l > 1 ? cal(n, s, l-1) : 0) << endl;
	return 0;
}

C. Jon Snow and his Favourite Number

对长度为n的序列进行k次这样的操作: 排序, 将排在奇数位的数与x异或. 问结束后最大和最小数是什么. (1≤n≤10^5, 0≤k≤10^5, 0≤ai,x≤10^3)

企图观察性质的话......会发现有循环节, 或者像我一样GG.

然而这是一道暴力. ai, x都不超过10^3, 意味着序列中不会出现大于等于2^10=1024的数. 维护一下每种数有多少个就好. 10^8这个级别好像有点卡, 所以给了4s.

当时我的想法是, 把有序的数按位取反, 大小顺序会反过来. 异或是对特定的一些位取反, 是不是可以把这些位与其他位剥开看呢? 有没有可能只去追踪几个数的位置? 行不通......

启示: 题面中有哪些较小的数? 能否加以利用?

#include <bits/stdc++.h>
#define Rep(i, a, b) for (int i = (a), i##_ = (b); i < i##_; ++i)
#define For(i, a, b) for (int i = (a), i##_ = (b); i <= i##_; ++i)

using namespace std;
typedef long long ll;

const int MAX_N = 1e5, MAX_X = 1<<10;

int a[MAX_N], b[MAX_N], c[MAX_N];

int main()
{
	int n, k, x;
	cin >> n >> k >> x;
	Rep (i, 0, n) {
		cin >> a[i];
		++b[a[i]];
	}
	Rep (i, 0, k) {
		copy(b, b + MAX_X, c);
		int cnt = 0;
		Rep (i, 0, MAX_X) {
			int t = cnt & 1 ? c[i]/2 : (c[i]+1)/2;
			b[i] -= t;
			b[i^x] += t;
			cnt += c[i];
		}
	}
	int mn = 0, mx = MAX_X - 1;
	while (!b[mn])
		++mn;
	while (!b[mx])
		--mx;
	cout << mx << " " << mn << endl;
	return 0;
}

D. Jon and Orbs

有k种宝珠, 最开始没有, 现在每天等概率生成一种, q个询问, 问至少多少天集齐k种的可能性不小于. (1≤k,q,pi≤1000)

如果理解了题意, 是一道DP. 设为前i天收集j种的概率, 转移即可. 回答询问时二分, 或者提前预处理所有答案. 发现第k天时这个概率可能小到10的几千次方分之一, 为了防止精度问题, 用了long double, 还想会不会需要取个对数之类的. 官方教程直接double......

由于最多问到1/2, 实验表明计算前7300天足够.

#include <bits/stdc++.h>
#define Rep(i, a, b) for (int i = (a), i##_ = (b); i < i##_; ++i)
#define For(i, a, b) for (int i = (a), i##_ = (b); i <= i##_; ++i)

using namespace std;
typedef long double ld;

const int MAX_N = 7300, MAX_K = 1e3;
const ld eps = 1e-7;

ld f[MAX_N + 1][MAX_K + 1], g[MAX_N - MAX_K + 1];

int main()
{
	int k, q, n;
	cin >> k >> q;
	f[1][1] = 1;

	For (i, 2, MAX_N) {
		For (j, 1, min(k, i))
			f[i][j] = j*f[i-1][j]/k + (k-j+1)*f[i-1][j-1]/k;
		if (f[i][k] > 0.501) {
			n = i;
			break;
		}
	}

	For (i, k, n)
		g[i-k] = f[i][k];
	
	while (q--) {
		int p;
		cin >> p;
		cout << upper_bound(g, g+n-k+1, (p-eps)/2000) - g + k << endl;
	}
	
	return 0;
}

int main()
{
	int k, q, n;
	cin >> k >> q;
	f[1][1] = 1;

	For (i, 2, MAX_N) {
		For (j, 1, min(k, i))
			f[i][j] = j*f[i-1][j]/k + (k-j+1)*f[i-1][j-1]/k;
		if (f[i][k] > 0.501) {
			n = i;
			break;
		}
	}

	For (i, k, n)
		g[i-k] = f[i][k];
	
	while (q--) {
		int p;
		cin >> p;
		cout << upper_bound(g, g+n-k+1, (p-eps)/2000) - g + k << endl;
	}
	
	return 0;
}

E. Game of Stones

n堆Nim游戏, 每堆si, 限制每堆不能同一规模多于一次, 问后手是否能赢. (1≤n≤10^6, 1≤si≤60)

算是我做的第一道游戏题. 重新看了一遍理论知识, 这题就是SG定理的应用. 但是它的SG函数非常有趣.


SG函数: SG(x) = mex { SG(y) | y是x的后继状态 } mex是不在集合中的最小自然数. 终态的SG函数值为0. 性质: x是必败状态 <=> SG(x) = 0 证明: 将所有状态拓扑排序, 运用数学归纳法. 1) 对于终止状态, 命题为真. 2) 如果x是必败状态, 那么它的后继都是必胜状态, 由归纳假设, 其SG值大于0, 所以SG(x)=0. 另一方面, 如果x是必胜状态, 那么它存在一个后继是必败状态, 其SG值等于0, 所以SG(x)<0.

SG定理: 游戏的和的SG函数值等于各个游戏SG值的Nim和 (异或和).

单堆Nim游戏的SG函数值等于石子数. Bouton定理可视为SG定理在Nim游戏中的直接应用.


于是, 本题可以算出在每一堆上游戏的SG函数值, 异或起来, 后手有必胜策略当且仅当异或和为0.

由于si很小, 而且满足两两不等的拆分也不多, 可以写个DP然后打表. 以 (石子数, 能用的规模) 为状态, 记忆化搜索即可. 实际能用的最大规模不超过石子数, 可以此减少状态数.

得到的结果很奇怪, 然后WA了.

单步运行了一下DP程序, 发现求mex那里写错了. 我排了个序, 处理重复的数那里出了点问题. 用布尔数组是更方便的选择.

正确的表是这样的: 0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, ...

尽管不清楚原理.

是不是很神奇呢?

#include <bits/stdc++.h>
#define Rep(i, a, b) for (int i = (a), i##_ = (b); i < i##_; ++i)
#define For(i, a, b) for (int i = (a), i##_ = (b); i <= i##_; ++i)

using namespace std;
typedef long long ll;
typedef pair<int, ll> il;

const int inf = 1<<30;

map<il, int> f;

int sg(int x, ll S)
{
	if (!x || !S)
		return 0;
	
	il p(x, S);
	if (f.count(p))
		return f[p];

	vector<int> v;
	
	For (i, 1, x) {
		ll msk = 1LL << (i-1);
		if (S & msk)
			v.push_back(sg(x-i, S & ~msk & ((1LL << (x-i)) - 1)));
	}

	sort(v.begin(), v.end());

	if (v[0])
		return f[p] = 0;

	int t = v.back() + 1;

	Rep (i, 0, v.size() - 1)
		if (v[i+1] - v[i] > 1) {
			t = v[i] + 1;
			break;
		}

	return f[p] = t;
}

int main()
{
	For (i, 0, 60)
		cout << sg(i, (1LL<<i) - 1) << ",";
	return 0;
}
#include <bits/stdc++.h>
#define Rep(i, a, b) for (int i = (a), i##_ = (b); i < i##_; ++i)
#define For(i, a, b) for (int i = (a), i##_ = (b); i <= i##_; ++i)

using namespace std;

const int sg[61] = {0,1,1,2,2,2,3,3,3,3,4,4,4,4,4,5,5,5,5,5,5,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10};

int main()
{
	int n, x = 0;
	cin >> n;
	Rep (i, 0, n) {
		int s;
		cin >> s;
		x ^= sg[s];
	}
	cout << (x ? "NO" : "YES") << endl;
	return 0;
}

G. Barrels and boxes

f个一样的食物盒, w个一样的酒桶. 将它们摆成很多堆, 相同种类的堆不能相邻. Jon不喜欢一种摆法当且仅当存在高度 (即酒桶数目) 不超过h的酒堆, 每终摆法等可能, 输出他喜欢某种摆法的概率, 对(10^9+7)取模. (0≤f,w,h≤10^5)

把一个正整数n拆成有顺序的k个正整数之和的方案数是. 如果限制每部分大于自然数h, 就每部分先分配h个, 于是方案数为.

由于1拆成1个部分的方案数是1, 所以规定可与公式相容.

接下来可以枚举食物盒的堆数k, 然后讨论一下, 是k个, (k-1)个, 还是(k+1)个酒堆......

更优美的做法: 不要让它们竖着堆, 而是横着摆......所以分母是. 枚举酒桶的堆数k, 那么要在一排食物盒中选k个间隔放酒, 再乘上f拆分成k个有顺序且不大于h的部分的方案数X: .

一位外国友人发消息问 X 怎么计算 (大概是群发? ), 因为官方教程里用的是生成函数. 杀鸡不用宰牛刀啊. 于是提了一下 Generating Function 这个词和相关公式, 并解释了一下怎么从不定方程解的组数的角度看这个问题. 能帮到别人挺开心的~

#include <bits/stdc++.h>
#define Rep(i, a, b) for (int i = (a), i##_ = (b); i < i##_; ++i)
#define For(i, a, b) for (int i = (a), i##_ = (b); i <= i##_; ++i)
#define Down(i, a, b) for (int i = (a), i##_ = (b); i >= i##_; --i)
using namespace std;
typedef long long ll;

const int MAX_N = 1e5, MOD = 1e9 + 7;

int n;
ll fac[2][MAX_N + 1];

ll inverse(int x)
{
	return x == 1 ? 1 : MOD - MOD/x*inverse(MOD % x) % MOD;
}

void init()
{
	fac[0][0] = fac[1][0] = 1;
	For (i, 1, n)
		fac[0][i] = fac[0][i-1] * i % MOD;
	fac[1][n] = inverse(fac[0][n]);
	Down (i, n-1, 1)
		fac[1][i] = fac[1][i+1] * (i+1) % MOD;
}

inline ll C(ll n, ll k)
{
	return n < 0 || k < 0 || k > n ? 0 : fac[0][n] * fac[1][k] % MOD * fac[1][n-k] % MOD;
}

int main()
{
	ll f, w, h;
	cin >> f >> w >> h;
	if (!f) {
		cout << (w > h) << endl;
		return 0;
	}
	if (!w) {
		cout << "1" << endl;
		return 0;
	}
	n = max(f, w) - 1;
	init();

	ll p = 0, q = 0; // p/q

	For (i, 1, f) {
		ll t = C(f-1, i-1);
		(q += t * (2*C(w-1, i-1) + C(w-1, i) + C(w-1, i-2))) %= MOD;
		(p += t * (2*C(w-i*h-1, i-1) + C(w-(i+1)*h-1, i) + C(w-(i-1)*h-1, i-2))) %= MOD;
	}

	cout << p * inverse(q) % MOD << endl;

	return 0;
}

G. The Winds of Winter

一棵n个结点的有根树, 去掉某个结点, 则形成一片森林, 有一次改变某结点父亲的机会, 问去掉点i (i=1,2,..,n) 后森林里最大的树的最小结点数是多少. (1≤n≤10^5)

显然我们要将最大的树的某棵子树移植到最小的树上. 如果有多棵最大的树, 则任何移植都是无效的. 选择的子树过大, 小树会变得太大; 选择的子树过小, 大树仍然很大. 设大树的大小为M, 小树的大小为m, 则我们要最小化, Y是大树某棵子树的大小. 按照的大小关系分类, 二分查找即可.

所以, 现在的问题是维护去掉某个结点形成的每棵树的所有子树的大小, 支持lower_bound操作. 如果不考虑该结点上方那些就好搞了, 直接multiset启发式合并. (那就不会作为一场CF的G题了......)

据说, 遇到不会的题要使劲想, 没能解决的就是你该学的.

于是想了想......还是不知道怎样维护外面那些. 树上的一次转移似乎会使这个集合变化太多.

学了一下官方教程.

外面那些可以分为两类: parentouter. 其中parent中存的大小要减去当前子树的大小才是真实值. outer定义为parent和当前子树的补集, 于是, 所有操作仅仅是在三个集合之间移动元素, 这和直接添加元素的时间复杂度是相同的. . 补集是一种可以顺便维护的东西.

写略复杂的DFS之前想清定义. 相当于隐含使用数学归纳法证明算法的正确性. 这里的定义是, 调用前, 全集U = parent + outer, 调用后, U = parent + outer + S, 集合两两不交. 如果调用后仅仅返回子树的一份拷贝而不修改outer, 会发现二分的时候需要将整棵子树从outer中去除, 这样时间复杂度是不对的.

multiset erase一个值会删除所有具有该值的元素, 所以得先find.

第一次提交没有将次大的儿子与外部的大小做比较. 维护了次大值的时候不要忘记啊. 还忘了一件事: 和std对拍, 没加srand......

#include <bits/stdc++.h>
#define Rep(i, a, b) for (int i = (a), i##_ = (b); i < i##_; ++i)
#define For(i, a, b) for (int i = (a), i##_ = (b); i <= i##_; ++i)
#define Down(i, a, b) for (int i = (a), i##_ = (b); i >= i##_; --i)
#define ALL(s) s.begin(), s.end()

using namespace std;
const int MAX_N = 1e5;
typedef multiset<int> Set;
typedef Set::iterator iter;

int n, sz[MAX_N + 1], ans[MAX_N + 1];
vector<int> adj[MAX_N + 1];
Set s[MAX_N + 1], parent, outer;

void get_size(int u)
{
	sz[u] = 1;
	Rep (i, 0, adj[u].size()) {
		int v = adj[u][i];
		get_size(v);
		sz[u] += sz[v];
	}
}

inline bool get_big(int u, int& mx_sz, int& se_sz, int& mn_sz, int& big_ch)
{
	se_sz = mx_sz = 0;
	mn_sz = n + 1;
	big_ch = -1;
	int cnt = 1;
	Rep (i, 0, adj[u].size()) {
		int v = adj[u][i];
		if (sz[v] > mx_sz) {
			mx_sz = sz[v];
			big_ch = v;
			cnt = 1;
		} else if (sz[v] == mx_sz)
			++cnt;
		mn_sz = min(mn_sz, sz[v]);
	}

	Rep (i, 0, adj[u].size()) {
		int v = adj[u][i];
		if (v != big_ch)
			se_sz = max(se_sz, sz[v]);
	}

	return cnt != 1;
}

inline int get_ans(int mn, int mx, Set& S, int offset=0)
{
	int ans = mx;
	iter y = S.lower_bound((mx-mn+1)/2 - offset);
	if (y != S.end())
		ans = min(ans, mn + *y + offset);
	if (y != S.begin())
		ans = min(ans, mx - *--y - offset);
	return ans;
}

inline void remove(Set& s, int v)
{
	s.erase(s.find(v));
}

inline void remove(Set& s, Set& t)
{
	for (iter i = t.begin(); i != t.end(); ++i)
		remove(s, *i);
}

// before: U = parent + outer
// after: U = S + parent + outer
void dfs(int u, Set& S)
{
	if (sz[u] == 1) {
		remove(outer, sz[u]);
		S.insert(sz[u]);
		ans[u] = n-1;
		return;
	}

	int mx, mn, se, big;
	bool eq = get_big(u, mx, se, mn, big);

	remove(outer, sz[u]);
	parent.insert(sz[u]);

	Rep (i, 0, adj[u].size()) {
		int v = adj[u][i];
		if (v != big) {
			dfs(v, s[v]);
			outer.insert(ALL(s[v]));
		}
	}

	dfs(big, S);

	remove(parent, sz[u]);

	Rep (i, 0, adj[u].size()) {
		int v = adj[u][i];
		if (v != big)
			remove(outer, s[v]);
	}

	if (mx == n-sz[u] || mx > n-sz[u] && eq)
		ans[u] = mx;
	else if (n-sz[u] > mx)
		ans[u] = max(adj[u].size() == 1 ? 0 : mx, min(get_ans(mn, n-sz[u], outer), get_ans(mn, n-sz[u], parent, -sz[u])));
	else  // mx > n-sz[u] && !eq
		ans[u] = max(adj[u].size() == 1 ? 0 : max(se, n-sz[u]), get_ans(min(mn, n-sz[u] ? n-sz[u] : n+1), mx, S));

	Rep (i, 0, adj[u].size()) {
		int v = adj[u][i];
		if (v != big) {
			S.insert(ALL(s[v]));
			s[v].clear();
		}
	}

	S.insert(sz[u]);
}

int main()
{
	cin >> n;
	Rep (i, 0, n) {
		int u, v;
		cin >> u >> v;
		adj[u].push_back(v);
	}

	get_size(adj[0][0]);

	For (i, 1, n)
		outer.insert(sz[i]);

	dfs(adj[0][0], s[1]);

	For (i, 1, n)
		cout << ans[i] << endl;

	return 0;
}