【集训队作业】IOI 2020 集训队作业 试题泛做 5

Atcoder Regular Contest 89

Problem F. ColoringBalls

首先考虑如何判断一个给定的序列是否能够得到。

显然,连续的相同颜色可以被替换为一个,并且,每一段不包含 W 的区间都可以分开考虑。

可以发现,分开的区间分为如下几种:
( 1 ) (1) (1) R R R ,需要操作序列: r r r
( 2 ) (2) (2) B , R B , B R B,RB,BR B,RB,BR ,需要操作序列: r b rb rb
( 3 ) (3) (3) B R B , R B R B , B R B R , R B R B R BRB,RBRB,BRBR,RBRBR BRB,RBRB,BRBR,RBRBR ,需要操作序列: r b ? rb? rb?
( 4 ) (4) (4) B R B R B , R B R B R B , B R B R B R , R B R B R B R BRBRB,RBRBRB,BRBRBR,RBRBRBR BRBRB,RBRBRB,BRBRBR,RBRBRBR ,需要操作序列: r b ? ? rb?? rb??
⋯ \cdots

不妨直接枚举每一段区间的种类 f f f ,判断 s s s 中是否能够取出所需的操作序列,如果可以,计算可能的结果序列数,计入答案。

在判断是否能够取出所需的操作序列时,可以考虑贪心,即优先选取在 s s s 中靠前的 r , b r,b r,b ,然后按照需要的 ? ? ? 数量从少到多将区间的操作序列从后向前排列,判断 ? ? ? 数量是否足够。

由此,我们得到了一个 O ( P ( N ) × P o l y ( N ) ) O(P(N)\times Poly(N)) O(P(N)×Poly(N)) 的做法,其中 P ( N ) P(N) P(N) N N N 的拆分数。

进一步地考虑,我们可以用动态规划优化上述过程,考虑枚举 f f f 中的元素个数,并从小到大向 f f f 中加入元素。

在状态中计入 l f t lft lft 序列中剩余的元素个数、 p o s pos pos 剩余需要决策的区间数、 o p t opt opt 剩余的 ? ? ? 个数、 l a s t last last 当前的区间种类、 c n t cnt cnt 当前区间种类连续出现的次数。

由于可以在计算答案时乘上组合数计算,可以将连续的相同字符压缩为一个,从而转移是 O ( 1 ) O(1) O(1) 的。令 N , K N,K N,K 同阶,单次 DP 的总状态数为 O ( N 4 L o g N ) O(N^4LogN) O(N4LogN) ,且常数极小。

总时间复杂度为 O ( N 5 L o g N ) O(N^5LogN) O(N5LogN)

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 75;
const int P = 1e9 + 7;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
int fac[MAXN], inv[MAXN], invn[MAXN];
int power(int x, int y) {
	if (y == 0) return 1;
	int tmp = power(x, y / 2);
	if (y % 2 == 0) return 1ll * tmp * tmp % P;
	else return 1ll * tmp * tmp % P * x % P;
}
int binom(int x, int y) {
	if (y > x) return 0;
	else return 1ll * fac[x] * inv[y] % P * inv[x - y] % P;
}
void update(int &x, int y) {
	x += y;
	if (x >= P) x -= P;
}
char s[MAXN];
int n, m, p[MAXN], q[MAXN], r[MAXN], sum[MAXN];
unordered_map <int, int> dp[MAXN][MAXN][MAXN];
int split(int x, int y) {
	if (y > x) return 0;
	else return binom(x - 1, y - 1);
}
inline int Hash(int x, int y) {
	return x * MAXN + y;
}
int getdp(int lft, int pos, int opt, int last, int cnt) {
	if (pos == 0) return lft <= 1;
	if (last > opt + 1) return 0;
	if ((last + 1) * pos > lft) return 0;
	if (last * pos > opt + sum[pos]) return 0;
	if (dp[lft][pos][opt].count(Hash(last, cnt))) {
		return dp[lft][pos][opt][Hash(last, cnt)];
	}
	int &ans = dp[lft][pos][opt][Hash(last, cnt)];
	ans = getdp(lft, pos, opt, last + 1, 0);
	if (last == 0) {
		if (lft >= 2) {
			update(ans, 1ll * invn[cnt + 1] * 
			                  getdp(lft - 2, pos - 1, opt + r[pos] + (q[pos] != 0), last, cnt + 1) % P);
		}
	} else if (q[pos] != 0) {
		int base = 2 * last;
		if (lft >= base) {
			update(ans, 1ll * invn[cnt + 1] *
			                  getdp(lft - base, pos - 1, opt + r[pos] + 1 - last, last, cnt + 1) % P);
		}
		if (lft >= base + 1) {
			update(ans, 2ll * invn[cnt + 1] *
			                  getdp(lft - base - 1, pos - 1, opt + r[pos] + 1 - last, last, cnt + 1) % P);
		}
		if (lft >= base + 2) {
			update(ans, 1ll * invn[cnt + 1] *
			                  getdp(lft - base - 2, pos - 1, opt + r[pos] + 1 - last, last, cnt + 1) % P);
		}
	}
	return ans;
}
int func(int cnt) {
	static bool vis[MAXN]; int lft = 0;
	memset(vis, false, sizeof(vis));
	for (int i = 1; i <= m; i++)
		if (s[i] == 'r' && lft != cnt) {
			p[++lft] = i, q[lft] = 0;
			vis[i] = true;
			for (int j = i; j <= m; j++)
				if (!vis[j] && s[j] == 'b') {
					vis[j] = true;
					q[lft] = j;
					break;
				}
		}
	if (lft != cnt) return 0;
	for (int i = 1; i <= m; i++) {
		r[i] = 0;
		for (int j = q[i - 1] + 1; j <= q[i] - 1; j++)
			r[i] += !vis[j];
		sum[i] = sum[i - 1] + r[i] + vis[p[i]] + vis[q[i]];
	}
	int cmt = 0;
	for (int i = m; i >= 1; i--)
		if (vis[i]) break;
		else cmt++;
	for (int i = 0; i < MAXN; i++)
	for (int j = 0; j < MAXN; j++)
	for (int k = 0; k < MAXN; k++)
		dp[i][j][k].clear();
	int ans = 0;
	for (int i = 1; i <= n; i++)
		update(ans, 1ll * (getdp(i, cnt, cmt, 0, 0) + getdp(i + 1, cnt, cmt, 0, 0)) * split(n, i) % P);
	return 1ll * ans * fac[cnt] % P;
}
int main() {
	read(n), read(m);
	scanf("\n%s", s + 1);
	int ans = 0;
	fac[0] = inv[0] = 1;
	for (int i = 1; i <= max(n, m); i++) {
		fac[i] = 1ll * fac[i - 1] * i % P;
		inv[i] = power(fac[i], P - 2);
		invn[i] = power(i, P - 2);
	}
	for (int i = 0; i <= m; i++)
		update(ans, func(i));
	writeln(ans);
	return 0;
}

Atcoder Regular Contest 91

Problem F. Strange Nim

考虑对于固定的 K K K ,计算 A = 0 , 1 , 2 , … A=0,1,2,\dots A=0,1,2, 时的 SG 函数值。

可以发现 SG 函数 s g ( A ) sg(A) sg(A) 具有如下规律:
( 1 ) (1) (1) 、对于 A = i k A=ik A=ik s g ( A ) = i sg(A)=i sg(A)=i
( 2 ) (2) (2) 、去掉所有 A = i k A=ik A=ik A A A ,将剩余位置按顺序排列, SG 函数列表不变。

可以用归纳法简单地证明以上规律。

由该规律,不难得到一个 O ( K L o g A ) O(KLogA) O(KLogA) 的计算单堆石子 SG 函数的算法。

并且,可以对连续的删除进行优化,得到一个 O ( A K ) O(\frac{A}{K}) O(KA) 的计算单堆石子 SG 函数的算法。

在两者中取较优的时间复杂度,则总时间复杂度为 O ( N V L o g V ) O(N\sqrt{VLogV}) O(NVLogV )

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5;
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
int sg(int x, int y) {
	if (x % y == 0) return x / y;
	int div = x / y, mns = div + 1;
	int delta = x - y * div, cnt = delta / mns + (delta % mns != 0);
	return sg(x - mns * cnt, y);
}
int main() {
	int n, ans = 0; read(n);
	for (int i = 1; i <= n; i++) {
		int x, y; read(x), read(y);
		ans ^= sg(x, y);
	}
	if (ans) puts("Takahashi");
	else puts("Aoki");
	return 0;
}

Atcoder Regular Contest 92

Problem F. Two Faced Edges

考虑翻转一条边 $ a\Rightarrow b $ 会使强联通分量如何变化。

首先,我们断开了一条边 $ a\Rightarrow b $ ,若原本 $ b $ 可以到达 $ a $ ,但 $ a $ 无法在不通过 $ a\Rightarrow b $ 的情况下到达 $ b $ ,那么强联通分量的个数将会增加。

其次,我们新增了一条边 $ b\Rightarrow a $ ,若原本 $ b $ 无法到达 $ a $ ,而 $ a $ 可以在不通过 $ a\Rightarrow b $ 的情况下到达 $ b $ ,那么强联通分量的个数将会减少。

因此,我们需要回答的问题本质上是:
1 、对于每一条边 $ a\Rightarrow b $ ,确定 $ b $ 是否可以到达 $ a $ 。
2 、对于每一条边 $ a\Rightarrow b $ ,确定 $ a $ 是否可以在不通过 $ a\Rightarrow b $ 的情况下到达 $ b $ 。

对于 1 ,我们从每个点开始做一遍 DFS 即可确定每个点是否能走到其它点。

对于 2 ,我们枚举 $ a $ ,并试图在 $ O(M) $ 的时间内确定 $ a $ 的每一条出边的答案。

不妨令 $ a $ 的出点为 $ b_1,b_2,…,b_m $ ,我们进行下列操作:
将从 $ b_1 $ 出发,能够不经过 $ a $ 而到达的点标记为1。
将从 $ b_2 $ 出发,能够不经过 $ a $ 和被标记过的点而到达的点标记为2。
……
将从 $ b_m $ 出发,能够不经过 $ a $ 和被标记过的点而到达的点标记为 $ m $ 。
按照 $ b_m,…,b_2,b_1 $ 的顺序再进行一遍标号。

两次标号相同的点(显然若两次标号相同,标号即为其下标)就是 $ a $ 不可以在不通过 $ a\Rightarrow b $ 的情况下到达 $ b $ 的点,其余的就是 $ a $ 可以在不通过 $ a\Rightarrow b $ 的情况下到达 $ b $ 的点。

时间复杂度 $ O(N\times M) $ 。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int MAXM = 2e5 + 5;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
struct edge {int dest, home; };
int n, m;
int pre[MAXN], suf[MAXN];
bool vis[MAXN][MAXN], ans[MAXM];
vector <edge> a[MAXN];
void dfs(int from, int pos) {
	vis[from][pos] = true;
	for (unsigned i = 0; i < a[pos].size(); i++)
		if (!vis[from][a[pos][i].dest]) dfs(from, a[pos][i].dest);
}
void dfspre(int pos, int val) {
	pre[pos] = val;
	for (unsigned i = 0; i < a[pos].size(); i++)
		if (pre[a[pos][i].dest] == -1) dfspre(a[pos][i].dest, val);
}
void dfssuf(int pos, int val) {
	suf[pos] = val;
	for (unsigned i = 0; i < a[pos].size(); i++)
		if (suf[a[pos][i].dest] == -1) dfssuf(a[pos][i].dest, val);
}
int main() {
	read(n), read(m);
	for (int i = 1; i <= m; i++) {
		int x, y; read(x), read(y);
		a[x].push_back((edge) {y, i});
	}
	for (int i = 1; i <= n; i++)
		dfs(i, i);
	for (int i = 1; i <= n; i++)
	for (unsigned j = 0; j < a[i].size(); j++)
		if (vis[a[i][j].dest][i]) ans[a[i][j].home] ^= true;
	for (int i = 1; i <= n; i++) {
		memset(pre, -1, sizeof(pre));
		memset(suf, -1, sizeof(suf));
		pre[i] = suf[i] = 0;
		for (unsigned j = 0; j < a[i].size(); j++)
			if (pre[a[i][j].dest] == -1) dfspre(a[i][j].dest, j);
		for (unsigned j = a[i].size(); j > 0; j--)
			if (suf[a[i][j - 1].dest] == -1) dfssuf(a[i][j - 1].dest, j - 1);
		for (unsigned j = 0; j < a[i].size(); j++)
			if (pre[a[i][j].dest] != suf[a[i][j].dest]) ans[a[i][j].home] ^= true;
	}
	for (int i = 1; i <= m; i++)
		if (ans[i]) printf("diff\n");
		else printf("same\n");
	return 0;
}

Atcoder Regular Contest 93

Problem E. Bichrome Spanning Tree

先求出原图的最小生成树 $ T $ ,令其权值为 $ S $ 。

显然,若 $ X<S $ ,答案为零。

考虑 $ X=S $ 的情况,我们要求的是“使得存在一棵最小生成树的树边颜色不全相同的染色方案数”。

对于每一条边,默认它被选取,做一遍最小生成树,如果结果为 $ S $ ,则说明它可以在最小生成树上,否则不可以。

令可以在最小生成树上的边数为 $ a $ ,其余边数为 $ b $ ,答案应为 $ (2^a-2)\times 2^b $ 。

考虑 $ X>S $ 的情况,我们要求的是“使得小于 $ X $ 的生成树上的边均为一种颜色,并且使得存在一棵权值为 $ X $ 生成树的树边颜色不全相同的染色方案数”。

显然 $ T $ 中的边需要是一种颜色。

对于其余边,记 $ F_i $ 为默认边 $ i $ 被选取时最小生成树的大小,由定义有 $ F_i≥S $ 。

若 $ F_i<X $ ,那么边 $ i $ 必须与 $ T $ 中的边同色。

记 $ F_i=X $ 的边数为 $ a $ , $ F_i>X $ 的边数为 $ b $ 。

我们需要至少在 $ F_i=X $ 的边中让一条边与 $ T $ 中的边反色。

$ F_i>X $ 的边可以任意涂色。

因此答案为 $ 2\times(2^a-1)\times 2^b $ 。

时间复杂度 $ O(M^2) $ 。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
const int P = 1e9 + 7;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
int n, m; long long s, mst;
int f[MAXN], x[MAXN], y[MAXN], z[MAXN], pos[MAXN];
int F(int x) {
	if (f[x] == x) return x;
	else return f[x] = F(f[x]);
}
long long MinimalSpanningTree() {
	for (int i = 1; i <= n; i++)
		f[i] = i;
	long long ans = 0;
	for (int i = 1; i <= m; i++) {
		int tmp = pos[i];
		int tx = F(x[tmp]), ty = F(y[tmp]);
		if (tx != ty) {
			f[tx] = ty;
			ans += z[tmp];
		}
	}
	return ans;
}
long long MinimalSpanningTree(int fixed) {
	for (int i = 1; i <= n; i++)
		f[i] = i;
	long long ans = z[fixed];
	f[x[fixed]] = y[fixed];
	for (int i = 1; i <= m; i++) {
		int tmp = pos[i];
		int tx = F(x[tmp]), ty = F(y[tmp]);
		if (tx != ty) {
			f[tx] = ty;
			ans += z[tmp];
		}
	}
	return ans;
}
int power(int x, int y) {
	if (y == 0) return 1;
	int tmp = power(x, y / 2);
	if (y % 2 == 0) return 1ll * tmp * tmp % P;
	else return 1ll * tmp * tmp % P * x % P;
}
bool cmp(int x, int y) {
	return z[x] < z[y];
}
int main() {
	freopen("ARC093E.in", "r", stdin);
	freopen("ARC093E.out", "w", stdout);
	read(n), read(m), read(s);
	for (int i = 1; i <= m; i++)
		read(x[i]), read(y[i]), read(z[i]), pos[i] = i;
	sort(pos + 1, pos + m + 1, cmp);
	mst = MinimalSpanningTree();
	if (mst > s) {
		printf("%d\n", 0);
		return 0;
	}
	if (mst == s) {
		int cnt = 0;
		for (int i = 1; i <= m; i++)
			cnt += MinimalSpanningTree(i) != mst;
		printf("%d\n", (power(2, m) - power(2, cnt + 1) + P) % P);
		return 0;
	}
	int cnt = 0, bnt = 0;
	for (int i = 1; i <= m; i++) {
		long long tmp = MinimalSpanningTree(i);
		if (tmp == s) bnt++;
		else if (tmp > s) cnt++, bnt++;
	}
	printf("%d\n", 2ll * (power(2, bnt) - power(2, cnt) + P) % P);
	return 0;
}

Problem F. Dark Horse

我们发现 1 号选手在任何位置的情况都是对称的,因此我们考虑 1 号选手在 1 号位的情况,并将答案乘以 $ 2^N $ 。

我们需要求出 2 到 $ 2^N $ 的排列 $ p_2,p_3,…,p_{2^N} $ 个数,使得
$ Min_1=p_2,Min_2=min{p_3,p_4},Min_3=min{p_5,p_6,p_7,p_8}…Min_N=min{p_{2{N-1}+1},p_{2{N-1}+2},…,p_{2^N}} $
均不在给出的 $ M $ 个数当中。

考虑容斥原理,我们选定一个 $ U={1,2,3,…,N} $ 的子集 $ S $ ,计算对于 $ i\in S $ , $ Min_i $ 在出的 $ M $ 个数当中的排列数 $ f_S $ ,那么答案即为 $ \sum_{S\in U}(-1)^{|S|}\times f_S $ 。

考虑如何计算 $ f_S $ ,不妨令 $ A_i $ 按降序排列。

我们依次考虑每个 $ A_i $ ,对于每一个 $ A_i $ ,我们做如下两个决策之一:
1 、选取一些尚未选取的大于 $ A_i $ 的数,组成一个大小为 $ 2^K $ 的集合( $ K $ 尚未被选取),将 $ K $ 标记为被选取。
2 、不进行选取。

如此求出的 $ dp_{i,S} $ 表示考虑了前 $ i $ 个 $ A_i $ ,被选取的集合为 $ S $ ,并且集合内的排列顺序已经确定的方案数。

那么有 $ f_S=dp_{M,S}\times rest_S! $ ,其中 $ rest_S $ 表示没有被选取的元素个数。

时间复杂度 $ O(2^N\times N\times M) $ 。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 20;
const int MAXS = 65536 + 5;
const int P = 1e9 + 7;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
int n, m, a[MAXN], bit[MAXN], c[MAXS];
int fac[MAXS], inv[MAXS], dp[MAXN][MAXS];
int power(int x, int y) {
	if (y == 0) return 1;
	int tmp = power(x, y / 2);
	if (y % 2 == 0) return 1ll * tmp * tmp % P;
	else return 1ll * tmp * tmp % P * x % P;
}
void update(int &x, int y) {
	x = (x + y) % P;
}
int getc(int x, int y) {
	return 1ll * fac[x] * inv[y] % P * inv[x - y] % P;
}
int main() {
	read(n), read(m);
	for (int i = 1; i <= m; i++)
		read(a[i]);
	for (int i = 0; i <= n - 1; i++)
		bit[i] = 1 << i;
	reverse(a + 1, a + m + 1);
	int cnt = 1 << n; fac[0] = 1;
	for (int i = 1; i <= cnt; i++)
		fac[i] = 1ll * fac[i - 1] * i % P;
	inv[cnt] = power(fac[cnt], P - 2);
	for (int i = cnt - 1; i >= 0; i--)
		inv[i] = inv[i + 1] * (i + 1ll) % P;
	int u = (1 << n) - 1; dp[0][0] = 1;
	for (int i = 1; i <= m; i++)
	for (int s = 0; s <= u; s++) {
		int lft = cnt - a[i] + 1 - s;
		update(dp[i][s], dp[i - 1][s]);
		for (int j = 0; j <= n - 1; j++) {
			if (lft < bit[j]) break;
			if (bit[j] & s) continue;
			update(dp[i][s ^ bit[j]], 1ll * dp[i - 1][s] * getc(lft - 1, bit[j] - 1) % P * fac[bit[j]] % P);
		}
	}
	int ans = fac[cnt - 1]; c[0] = 1;
	for (int s = 1; s <= u; s++) {
		c[s] = -c[s - (s & -s)];
		update(ans, 1ll * c[s] * dp[m][s] * fac[cnt - 1 - s] % P);
	}
	ans = (1ll * ans * cnt % P + P) % P;
	writeln(ans);
	return 0;
}

Atcoder Regular Contest 95

Problem F. Permutation Tree

我们动态地考虑所给的构建排列树的过程:

向排列中依次加入1到 $ N $ 的所有数,每个数在加入之前会与当前出现的最靠右的数连边。

我们发现这样构建出来的树是一个毛毛虫。

不难发现当且仅当给定的树不是毛毛虫,问题无解。

考虑如何构造原始排列。

令 1 到 $ N $ 的所有数在排列中出现的位置分别为 $ pos_i $ ,那么毛毛虫的主链就对应了从 $ pos_1 $ 开始的一个单调增子序列的下标两两相连形成的链。

毛毛虫主链上的点除了在主链上两两相连以外,剩余的点度的贡献就来自原排列中在其之前的一段不是主链上的点与其所连的边。

因此主链上的点在排列上的位置实际上是确定的,而为了让字典序最小,每个主链上的点与在其之前的一段不是主链上的点在值域上应当连续。并且因为主链上的点要小于在其之前的一段不是主链上的点,因此应当让主链取值域内的最小值,在其之前的一段不是主链上的点所分配的数依次递增。

毛毛虫的主链有两个方向,因此我们需要从两个方向分别尝试构造排列,并取字典序较小者作为答案。

时间复杂度 $ O(N) $ ,实现时有一些细节需要注意。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
vector <int> a[MAXN];
bool dis[MAXN];
int n, d[MAXN], e[MAXN];
int ans[MAXN], bns[MAXN];
void solve(int start, int *ans) {
	int pos = start, last = 0, tot = 0;
	ans[++tot] = 1;
	while (pos != last) {
		int tmp = ++tot;
		for (int i = 1; i <= e[pos]; i++) {
			ans[tot] = tot + 1;
			tot++;
		}
		ans[tot] = tmp;
		tmp = pos;
		for (unsigned i = 0; i < a[pos].size(); i++)
			if (!dis[a[pos][i]] && a[pos][i] != last) {
				pos = a[pos][i];
				break;
			}
		last = tmp;
	}
	ans[++tot] = n;
}
void print(int *ans) {
	for (int i = 1; i <= n; i++)
		printf("%d ", ans[i]);
	printf("\n");
}
bool Less(int *ans, int *bns) {
	for (int i = 1; i <= n; i++) {
		if (ans[i] < bns[i]) return true;
		if (ans[i] > bns[i]) return false;
	}
	return false;
}
int main() {
	read(n);
	for (int i = 1; i <= n - 1; i++) {
		int x, y; read(x), read(y);
		a[x].push_back(y);
		a[y].push_back(x);
	}
	for (int i = 1; i <= n; i++)
		if (a[i].size() == 1) dis[i] = true;
	int Max = 0;
	for (int i = 1; i <= n; i++)
		if (!dis[i]) {
			for (unsigned j = 0; j < a[i].size(); j++)
				if (!dis[a[i][j]]) d[i]++;
				else e[i]++;
			chkmax(Max, d[i]);
		}
	if (Max >= 3) {
		printf("-1\n");
		return 0;
	}
	if (Max == 0) {
		ans[1] = 1;
		ans[n] = n;
		if (n >= 3) ans[n - 1] = 2;
		for (int i = 2, j = 3; i <= n - 2; i++, j++)
			ans[i] = j;
		print(ans);
		return 0;
	}
	for (int i = 1; i <= n; i++)
		if (d[i] == 1) e[i]--;
	for (int i = 1; i <= n; i++)
		if (d[i] == 1) {
			solve(i, ans);
			break;
		}
	for (int i = n; i >= 1; i--)
		if (d[i] == 1) {
			solve(i, bns);
			break;
		}
	if (Less(ans, bns)) print(ans);
	else print(bns);
	return 0;
}

Atcoder Regular Contest 96

Problem E. Everything on It

由容斥原理,有 $ Ans=\sum_{i=0}{N}(-1)i\times \binom{N}{i}\times cnt_i $ ,其中 $ cnt_i $ 为1到 $ i $ 号元素只选取至多一个的方案数。

对于 $ i+1 $ 号到 $ N $ 号物品,我们没有做额外的限制,因此对于与集合 $ {1,2,…,i} $ 交集相同的物品我们可以视为一种物品,每一种物品有 $ 2^{N-i} $ 个。

显然除了与集合 $ {1,2,…,i} $ 交集为空的一组物品以外,每组物品最多选取一个。

我们可以将这个过程看做将1到 $ i $ 号元素分成若干个无序的组,每一个元素至多被分在一个组中,也可以不分在任何组中,每一组中的元素是在同一次物品选取中被选中的。

记将 1 到 $ i $ 号元素分成恰好 $ j $ 个无序的组的方案数为 $ dp_{i,j} $ ,那么 $ dp_{i,j} $ 对 $ cnt_i $ 的贡献应当为 $ dp_{i,j}\times (2{N-i})j\times 2{2{N-i}} $ 。

剩余的问题就是如何计算 $ dp_{i,j} $ ,考虑第 $ i $ 个元素的归属:它可以自成一组,也可以加入到之前已有的一个组中或不加入任何组,因此 $ dp_{i,j}=dp_{i-1,j-1}+dp_{i-1,j}\times (j+1) $ 。

时间复杂度 $ O(N^2) $ 。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 3005;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
int n, ans, P;
int fac[MAXN], inv[MAXN], dp[MAXN][MAXN];
int c(int x, int y) {
	if (y > x) return 0;
	else return 1ll * fac[x] * inv[y] % P * inv[x - y] % P;
}
void update(int &x, int y) {
	x = (x + y) % P;
}
int power(int x, int y, int P) {
	if (y == 0) return 1;
	int tmp = power(x, y / 2, P);
	if (y % 2 == 0) return 1ll * tmp * tmp % P;
	else return 1ll * tmp * tmp % P * x % P;
}
int getdp(int x) {
	int mul = power(2, power(2, n - x, P - 1), P);
	int chs = power(2, n - x, P), now = 1, ans = 0;
	for (int i = 0; i <= x; i++) {
		update(ans, 1ll * dp[x][i] * now % P);
		now = 1ll * now * chs % P;
	}
	return 1ll * mul * ans % P;
}
int main() {
	read(n), read(P);
	fac[0] = 1;
	for (int i = 1; i <= n; i++)
		fac[i] = 1ll * fac[i - 1] * i % P;
	inv[n] = power(fac[n], P - 2, P);
	for (int i = n - 1; i >= 0; i--)
		inv[i] = inv[i + 1] * (i + 1ll) % P;
	dp[0][0] = 1;
	for (int i = 1; i <= n; i++)
	for (int j = 0; j <= i; j++) {
		if (j) dp[i][j] += dp[i - 1][j - 1];
		update(dp[i][j], dp[i - 1][j] * (j + 1ll) % P);
	}
	for (int i = 0; i <= n; i++)
		if (i & 1) update(ans, 1ll * (P - c(n, i)) * getdp(i) % P);
		else update(ans, 1ll * c(n, i) * getdp(i) % P);
	writeln(ans);
	return 0;
}

Problem F. Sweet Alchemy

我们对 $ c_i $ 进行差分,令 $ d_i=c_i-c_{p_i} $ ,那么原题中对 $ c_i $ 的限制可以等价地表示为 $ 0≤d_i≤D(i=2,3,…,N) $ 。

$ d_i $ 每增加 1 ,意味着我们需要购买 $ i $ 子树内所有物品各一份。

现在问题等价地转化为了背包问题: $ N $ 种物品,每种物品体积为 $ sum_i $ ,价值为 $ size_i $ 且除了第一种物品以外其余物品有数量限制 $ D $ ,求 $ X $ 体积的背包能够容纳的物品的最大价值总和。(其中 $ sum_i $ 为 $ i $ 子树内所有物品的代价总和, $ size_i $ 为 $ i $ 子树的大小)

注意到 $ size_i≤N≤50 $ ,考虑从 $ size_i $ 入手分析。

有一种直观的贪心:按照物品的“性价比”从高到低选取,其中“性价比”为价值与体积的比值。

由于物品不能够分割,这样的贪心不是完全正确的,但由这个思路,我们发现,若物品 $ i $ 的性价比高于物品 $ j $ ,且物品 $ j $ 选取了 $ size_i $ 个,我们不妨将这些物品替换为 $ size_j $ 个物品 $ i $ ,这样一来,物品的价值不变,但体积就减少了。

因此,低“性价比”的物品至多会被选取 $ N $ 件,我们拿出每种物品中 $ Min{N,D} $ 件,这样拿出物品的总价值不会超过 $ O(N^3) $ 。进行简单DP,求出 $ dp_i $ ,表示选取价值为 $ i $ 的物品所占的最少体积。

之后枚举在 DP 中选取物品的价值,并对剩余物品执行上述贪心,取最优解即可。

时间复杂度 $ O(N^5) $ 。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 55;
const int MCNT = 125005;
const int INF = 1e9 + 10;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); } 
template <typename T> void read(T &x) {
	x = 0; int f = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
	for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
	x *= f;
}
template <typename T> void write(T x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
	write(x);
	puts("");
}
long long limit, sum[MAXN];
long long dp[MAXN][MCNT];
int n, d, fa[MAXN], size[MAXN], pos[MAXN];
bool cmp(int x, int y) {
	return sum[x] * size[y] < sum[y] * size[x];
}
int main() {
	read(n), read(limit), read(d);
	for (int i = 1; i <= n; i++) {
		read(sum[i]);
		if (i != 1) read(fa[i]);
	}
	int cnt = 0;
	for (int i = n; i >= 1; i--) {
		sum[fa[i]] += sum[i];
		size[fa[i]] += ++size[i];
		cnt += size[i] * n;
		pos[i] = i;
	}
	sort(pos + 1, pos + n + 1, cmp);
	for (int i = 1; i <= cnt; i++)
		dp[0][i] = INF;
	int tmp = min(n, d);
	for (int i = 1; i <= n; i++) {
		memcpy(dp[i], dp[i - 1], sizeof(dp[i]));
		for (int j = 0; j <= cnt; j++)
		for (int k = 1; k <= tmp && j - k * size[i] >= 0; k++)
			chkmin(dp[i][j], dp[i - 1][j - k * size[i]] + k * sum[i]);
	}
	int ans = 0;
	for (int i = 0; i <= cnt; i++) {
		if (dp[n][i] > limit) continue;
		int tans = i, lft = limit - dp[n][i];
		for (int j = 1; j <= n; j++) {
			int tmp = pos[j], used = min(1ll * max(d - n, 0), lft / sum[tmp]);
			if (tmp == 1) used = lft / sum[tmp];
			lft -= sum[tmp] * used;
			tans += size[tmp] * used;
		}
		chkmax(ans, tans);
	}
	writeln(ans);
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值