C.DFS Game

  • 题意:n个点的树上,每个点都有一枚硬币。两人博弈,先手在一号点上。每次:当前点有硬币取走、换人。否则,如果存在有硬币的儿子,其中选择一个走一步;否则回到父亲。
    双方都希望自己得到的硬币数最多,问先手最后的硬币数。
  • 思路:
    感觉这个问题还挺有思维难度的,看这篇题解,很详细了,我也只会复述它的话。
    首先最基本的结论就是:进入sz为奇数儿子回来会交换先后手,。
    然后这又是个零和博弈,你作为先手(有主动权)一定要让自己尽量走:使先手比后手赢得尽量多的儿子(子问题)
    结合上面两点分类讨论……
  • code
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int fa[N], nxt[N], to[N], head[N], ecnt, sz[N];
void add_edge(int u, int v) {nxt[++ecnt] = head[u]; to[ecnt] = v; head[u] = ecnt;}
int dfs(int u) {
	sz[u] = 1;
	vector<int> V;
	int god = -1, bad = 0;
	for(int i = head[u]; i; i = nxt[i]) {
		int v = to[i], d = dfs(v);
		bool f = (sz[v] & 1);
		if(!f) {
			if(d > 0) {god += d;}
			else bad += d;
		}
		else {V.push_back(d);}
		sz[u] += sz[v];
	}
	sort(V.begin(), V.end());
	int sz = V.size();
	for(int i = sz; i; i--) {
		int x = V[i - 1];
		god += (((sz - i) & 1) ? -1 : 1) * x;
	}
	if(sz & 1) god -= bad;
	else god += bad;
	return god;
}
int main() {
	int n;
	scanf("%d", &n);
	for(int i = 2; i <= n; i++) {scanf("%d", &fa[i]); add_edge(fa[i], i);}
	printf("%d", (n - dfs(1)) >> 1);
	return 0;
}

D.Skate

  • 题意:从一个点朝四个方向出发,遇到第一个方块或者墙就停下来。问最少加多少个方块,使得从任意位置出发可以到达所有位置。

  • 思路:是我想不到的喵喵子思路~
    如果出发在外圈(靠墙),可以经过第一/最后一列/行。
    如果(x,y)是方块,那么你可以在这个位置刹车,即四个方向都可选,那么第x行能经过,就可以经过y列,反之也成立(是双向的)。
    用并查集维护行和列的互通情况
    需要加的块个数为min(行连通块数,列连通块数)-1。

  • code:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2005;
char s[N][N];
int fa[N];
int g_fa(int u) {return fa[u] == u ? u : fa[u] = g_fa(fa[u]);}
void Merge(int u, int v) {fa[g_fa(u)] = g_fa(v);}
bool mark[N];
int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n + m; i++) fa[i] = i;
	for(int i = 1; i <= n; i++) {scanf("%s", s[i] + 1);}
	Merge(1, n), Merge(1, n + 1), Merge(1, n + m);
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			if(s[i][j] == '#') {Merge(i, j + n);}
		}
	}
	int cnt = 0, ans = 0;
	for(int i = 1; i <= n; i++) if(!mark[g_fa(i)]) {cnt++; mark[g_fa(i)] = 1;}
	ans = cnt; cnt = 0;
	for(int i = 1; i <= n; i++) mark[g_fa(i)] = 0;
	for(int i = 1; i <= m; i++) if(!mark[g_fa(i + n)]) {cnt++; mark[g_fa(i + n)] = 1;}
	ans = min(ans, cnt);
	printf("%d", ans - 1);
	return 0;
}

E.Cigar Box

跟昨天考试的某道题有点像。

  • 题意:有一个初始排列ppi=i。每次可以从中选择一个放在两侧,给最终序列a,问用m步把p变成a的操作序列的方案数。
  • 思路:每个数最终所在位置由最后一次操作确定(或者它一直静止,保持在初始递增序列中)。而之前把它放在哪里都不重要,也不会对其它数产生影响。
    我们另每个值最后一次操作叫关键操作。在最终序列中:第一次前置的是l位置的数,后置是r位置的数。那么中间的(l,r)的数一直静止,因为它们不可能在l,r操作后操作(到两侧就再也进不来了),而如果在l,r之前操作,即可取代第一次前置(后缀)的地位,矛盾。
    因此顺序就是l,l1,l2....1r,r+1...n1,n同时进行。
    可以考虑dp,设fi,l,r:做了i次操作左边还剩l个未归位,右边剩r个。

fi,l,r=fi1,l+1,r+fi1,l,r+12(l+r)

发现其实lr可以并为一维,表示未归位的总个数,最后统计贡献的时候再用组合数考虑lr交错操作顺序。
还有一个问题:你需要贡献答案时,需要中间的(l,r)是单增序列,此时已知的是还操作开始时两边未归位个数,这样发现我们需要从结果到开头反着推(就像期望dp一样)。

fi,j=fi+1,j1+fi+1,jj2

初始:fm,0=1

  • code:
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3005;
const int mod = 998244353;
ll dp[N][N], C[N][N];
int a[N];
int main() {
	int n, m;
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) {scanf("%d", &a[i]);}
	dp[m][0] = 1;
	for(int i = m - 1; ~i; i--) {
		for(int j = 0; j <= n; j++) {
			dp[i][j] = 2ll * dp[i + 1][j] * j % mod;
			if(j) dp[i][j] = (dp[i + 1][j - 1] + dp[i][j]) % mod;
		}
	}
	for(int i = 0; i <= n; i++) {
		C[i][0] = 1;
		for(int j = 1; j <= i; j++) {
			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
		}
	}
	ll ans = 0;
	for(int i = 0; i <= n; i++) ans = (ans + dp[0][n] * C[n][i]) % mod;
	for(int i = 1; i <= n; i++) {
		for(int j = i; j <= n; j++) {
			if(j > i && a[j] < a[j - 1]) break;
			int len = j - i + 1;
			ans = (ans + dp[0][n - len] * C[n - len][i - 1]) % mod;
		}
	}
	printf("%lld", ans);
	return 0;
}