2023.8.3模拟

BlackCrow / 2023-08-07 / 原文

T1 跑路

Description

定义“好矩阵”为存在一条从左上角到右下角曼哈顿距离最短的路径满足只经过\(0\)\(01\)矩阵,可以进行一次操作对任意子矩阵取反,求最小的操作次数使给定的、大小为\(n \times m\)的矩阵变成“好矩阵”。
\(n, m \leq 1000\)

Solution

画两下不难发现一次操作可以使路径上连续的一段\(1\)变为\(0\),接下来就是板子般的\(dp\)了。

Code

#include<bits/stdc++.h>

using namespace std;

const int MaxN = 1010, INF = 1e9;

int n, m, a[MaxN][MaxN], f[MaxN][MaxN];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			scanf("%d", &a[i][j]), f[i][j] = INF;
	f[1][1] = (a[1][1] == 0)?1:2;
	for (int i = 2; i <= n; ++i)
		f[i][1] = f[i-1][1]+((a[i][1] != a[i-1][1])?1:0);
	for (int i = 2; i <= m; ++i)
		f[1][i] = f[1][i-1]+((a[1][i] != a[1][i-1])?1:0);
	for (int i = 2; i <= n; ++i)
		for (int j = 2; j <= m; ++j) {
			f[i][j] = min(f[i][j], f[i][j-1]+((a[i][j] != a[i][j-1])?1:0));
			f[i][j] = min(f[i][j], f[i-1][j]+((a[i][j] != a[i-1][j])?1:0));
		}
	printf("%d", f[n][m]/2);
	return 0;
}

T2 清扫([AGC010C]Cleaning)

link:https://atcoder.jp/contests/agc010/tasks/agc010_c

Description

给出一颗大小为\(n\)的树,树上每个点有权值\(a_{1 \sim n}\),每次可以选择两个叶子节点,使它们之间路径上的所有点权值\(-1\),问是否能使所有点的权值为零。

\(2 \leq n \leq 10^5, 0 \leq a_i \leq 10^9\)

Solution

观察到叶子节点的操作次数\(=\)叶子节点的权值,再通过这条性质推导非叶子节点的情况,发现情况无非两种:

  1. 叶子节点和同一子树内的点相连。
  2. 叶子节点与不同子树的点相连。

也可以参考这张图:
a

发现情况1对点\(u\)的贡献需要除以\(2\)(绿色路径),而情况二不用(红色路径),又因点\(u\)的权值是确定的,所以便可以计算出点\(u\)对上方节点的贡献。

实现代码是还需要注意下方节点最大值与下方节点总和对点\(u\)的总贡献范围限制, 因为如果有节点贡献大于总和的\(\frac{1}{2}\),就必须有部分红色路径。

选择一个非叶子节点为根\(dfs\)即可,时间复杂度\(O(n)\)

Code

#include<bits/stdc++.h>
using namespace std;

const int MaxN = 100010;

struct Blck
{int to, next;}e[MaxN<<1];

int ecnt, head[MaxN<<1];

void add(int u, int v)
{e[++ecnt].to = v; e[ecnt].next = head[u]; head[u] = ecnt;}

int deg[MaxN];
long long a[MaxN], f[MaxN];
bool flag;

void dfs(int x, int pre) {
	if (deg[x] == 1) {f[x] = a[x]; return ;}
	long long mx = 0;
	int y;
	for (int i = head[x]; i; i = e[i].next) {
		y = e[i].to;
		if (y != pre) dfs(y, x), f[x] += f[y], mx = max(mx, f[y]);
		if (flag) return ;
	}
	long long res;
	if (mx > f[x]-mx) res = f[x]-mx;
	else res = f[x]>>1;
	if (f[x]-res > a[x] || f[x] < a[x]) {
		printf("NO\n");
		flag = 1;
		return ;
	} f[x] -= 2ll*(f[x]-a[x]);
}

int t, n;

int main() {
	int u, v, root;
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	for (int i = 1; i < n; ++i) {
		scanf("%d%d", &u, &v);
		++deg[u]; ++deg[v];
		add(u, v); add(v, u);
	}
	if (n == 1) {
		printf("%s\n", (a[1] == 0)?"YES":"NO");
		return 0;
	}
	if (n == 2) {
		printf("%s\n", (a[1] == a[2])?"YES":"NO");
		return 0;
	}
	for (int i = 1; i <= n; ++i) if (deg[i] != 1) root = i;
	dfs(root, -1);
	if (!flag) {
		if (f[root]) printf("NO\n");
		else printf("YES\n");
	}
	return 0;
}

T3 定向([ARC092F] Two Faced Edges)

link:https://atcoder.jp/contests/arc092/tasks/arc092_f

Description

有一个\(N\)个点\(M\)条边的有向图。保证图中不存在重边和自环。试判断将每条边反向,其他边不变的情况下,图中强连通分量的数量是否改变。若改变,输出 diff,否则输出 same

\(1 \leq N \leq 10^3, 1 \leq M \leq 2 \times 10^5\)

Solution

改变一条边\(u \rightarrow v\)的方向会使强连通分量增加、减少或不变,对这三种情况讨论:

  1. 当强连通分量增加时,说明\(u, v\)在同一强连通分量中,且图中不存在一条从\(u\)\(v\)的路径(不包含边\(u \rightarrow v\))。
  2. 当强连通分量减少时,说明\(u, v\)不在同一强连通分量中,且图中存在一条从\(u\)\(v\)的路径(不包含边\(u \rightarrow v\))。
  3. 当强连通分量不变时,说明\(u, v\)不在同一强连通分量中,且图中不存在一条从\(u\)\(v\)的路径(不包含边\(u \rightarrow v\));或\(u, v\)在同一强连通分量中,且图中存在一条从\(u\)\(v\)的路径(不包含边\(u \rightarrow v\))。
    \(f(u, v)\)表示是否在同一强连通分量中,\(g(u, v)\)表示且图中是否存在一条从\(u\)\(v\)的路径(不包含边\(u \rightarrow v\)),不难发现当\(f(u, v) \oplus g(u, v) = 1\)时,边\(u \rightarrow v\)的答案为same,否则答案为diff

对于\(f(u, v)\),一次tarjan即可预处理,接下来考虑如何计算\(g(u, v)\)

发现可以通过翻转搜索时的边表顺序来确定是否存在边\(u \rightarrow v\),因此通过两次dfs,可以一次性预处理出所有的\(g(u, \_)\)

时间复杂度方面,每次dfs复杂度\(O(m)\),共搜索\(n\)次,总共\(O(n, m)\)

(听说还有一种bitset做法\(O(\frac{n^3}{w})\)鸽了

Code

#include<bits/stdc++.h>

using namespace std;

const int MaxN = 1010, MaxM = 200010;

bool vis[MaxN];
int dfn[MaxN], low[MaxN], st[MaxN], c[MaxN], num, top, cnt;
vector<int> scc[MaxN], G[MaxN];

void tarjan(int x) {
	dfn[x] = low[x] = ++num;
	st[++top] = x; vis[x] = 1;
	for (int i = 0; i < G[x].size(); ++i) {
		int v = G[x][i];
		if (!dfn[v]) {
			tarjan(v);
			low[x] = min(low[x], low[v]);
		} else if(vis[v])
			low[x] = min(low[x], dfn[v]);
	}
	if (dfn[x] == low[x]) {
		++cnt;
		int y;
		do {
			y = st[top--];
			vis[y] = 0;
			c[y] = cnt;
			scc[cnt].push_back(y);
		} while (x != y);
	}
}

int de1[MaxN], de2[MaxN], vis1[MaxN], vis2[MaxN];

void dfs(int x, int dep, int pre) {
	de1[x] = dep; vis1[x] = 1;
	for (int i = 0; i < G[x].size(); ++i) {
		int y = G[x][i];
		if (y != pre && !vis1[y])
			dfs(y, dep+1, x);
	}
}

void dfs2(int x, int dep, int pre) {
	de2[x] = dep; vis2[x] = 1;
	for (int i = G[x].size()-1; ~i; --i) {
		int y = G[x][i];
		if (y != pre && !vis2[y])
			dfs2(y, dep+1, x);
	}
}

int n, m, u[MaxM], v[MaxM], f[MaxN][MaxN], g[MaxN][MaxN];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; ++i) {
		scanf("%d%d", &u[i], &v[i]);
		G[u[i]].push_back(v[i]);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i);
	int sl;
	for (int i = 1; i <= cnt; ++i) {
		sl = scc[i].size();
		for (int j = 0; j < sl-1; ++j)
			for (int k = j+1; k < sl; ++k)
				f[scc[i][j]][scc[i][k]] = f[scc[i][k]][scc[i][j]] = 1;
	}
	for (int i = 1; i <= n; ++i) {
		memset(vis1, 0, sizeof(vis1));
		memset(vis2, 0, sizeof(vis2));
		dfs(i, 0, -1), dfs2(i, 0, -1);
		for (int j = 1; j <= n; ++j)
			if (j != i) g[i][j] = !(de1[j] == 1 && de2[j] == 1);
	}
	for (int i = 1; i <= m; ++i)
		printf("%s\n", (g[u[i]][v[i]]^f[u[i]][v[i]])?"diff":"same");
	return 0;
}

T4 染色([ARC089F]ColoringBalls)

link:https://atcoder.jp/contests/arc089/tasks/arc089_f

Description

\(N\)个白色的小球排成一排,有一个长为\(K\)的字符串\(S\)。接下来进行\(K\)次操作。

\(i\)个操作,选择一段连续的小球(可以为空),若\(S\)中第\(i\)个字符是\(r\),则将这些球染成红色;若是\(b\),则将它们染成蓝色。由于染料的特性,不能直接用蓝色来染白色。

求在进行完所有操作后,所有小球的颜色序列可以有多少种。

\(10^9+7\)取模。

\(1 \leq N,K \leq 70, |s|=K\)

Solution

如Kubic老师所言:“看到这种形式题目的一般都先转化为考虑对于一种结果,是否存在方案满足要求”,判断后我们在考虑如何计数。

判断是否存在方案满足要求

发现段的长短并不影响方案,因此一段段考虑结果。

白色球是初始状态,因此考虑将白球段作为分割点,那么剩下两种段,一种纯红色,一种红蓝混合。

显然红色段只需要一个\(r\)即可。

对于蓝色段,创造出第一段蓝色需要一个“\(\{r,b\}\)”,然后每一个字符都可以使蓝色段数\(+1\)(在蓝色段中插入红色或在初始红色段上添加蓝色)。

记第\(i\)\(\{r, b\}\)后未选择的字符数有\(t_i\)个,还需\(s_i\)个字符满足。

贪心地每次取最靠左的字符,那么充要条件便是

\[\forall i, t_i \geq \sum\limits^c_{j=i} s_j \]

显然\(s\)降序排列时最有可能满足条件。

枚举有\(a\)个纯红色段,\(c\)个混合段,判断是否有方案后,接下来就可以计数了。

DP计数

先鸽一小下

Code

#include<bits/stdc++.h>
using namespace std;

const int MaxN = 80, MaxC = 210;
const long long mod = 1e9+7;

long long C[MaxC][MaxC];

void getC() {
	for (int i = 0; i <= 200; ++i) {
		C[i][0] = 1;
		for (int j = 1; j <= i; ++j)
			C[i][j] = (C[i-1][j-1]+C[i-1][j])%mod; 
	}
}

char str[MaxN];
bool vis[MaxN];
int st[MaxN], n, k;

bool check(int c, int a) {															//判断状态是否合法(顺便处理所需杂色段rb位置) 
	int tot = 0, bpos; st[0] = 0;
	for (int i = 1; i <= k; ++i) if (str[i] == 'r') {
		if (tot < c) {																//加入杂色段 
			++tot;
			bpos = -1;
			for (int j = i+1; j <= k; ++j)
				if (str[j] == 'b' && !vis[j]) {bpos = j; break ;}
			if (!(~bpos)) return false;
			vis[i] = vis[bpos] = true;
			st[++st[0]] = bpos; 
		} else {
			if (tot < c+a) ++tot, vis[i] = true;									//加入纯色段
			else break ; 
		}
	}
	if (tot == a+c) return true ;
	else return false;
}

long long dp[MaxN][50][50], sum[MaxN], w[MaxN];

long long solve (int c, int a) {
	if (!c && !a) return 1;
	memset(vis, 0, sizeof(vis));
	if(!check(c, a)) return 0;
	memset(dp, 0, sizeof(dp));
	for (int i = k; i; --i) sum[i] = sum[i+1]+(!vis[i]);
	for (int i = 1; i <= c; ++i) w[i] = sum[st[c-i+1]]+i;
	long long s; dp[0][0][0] = 1;
	for (int i = 0; i < c; ++i) for (int j = 0; j <= w[i] && (j+a)*2-1 <= n; ++j)	//解决问题,还是只会仰仗dp
		for (int p = 1, s = dp[i][j][0]; j+p <= w[i+1] && (j+p+a)*2-1 <= n; ++p) {
			for (int l = 1; i+l <= c && j+p*l <= w[i+l] && (j+p*l+a)*2-1 <= n; ++l)
				dp[i+l][j+p*l][p] = (dp[i+l][j+p*l][p]+1ll*s*C[i+l][l]%mod)%mod;
			s = (s+dp[i][j][p])%mod;
		}
	int tmp1, tmp2; long long ans = 0;
	for (int i = 0; i <= w[c] && (i+a)*2-1 <= n; ++i) {
		s = 0, tmp1 = (i+a)*2-1, tmp2 = c*2+2;
		for (int j = 0; j <= i; ++j) s = (s+dp[c][i][j])%mod;
		ans = (ans+1ll*s*C[n+tmp2-1][tmp1+tmp2-1]%mod)%mod;
	} ans = 1ll*ans*C[a+c][c]%mod; return ans;
}

int main() {
	long long ans = 0;
	getC();
	scanf("%d%d%s", &n, &k, str+1);
	for (int c = 0; 2*c <= k; ++c)
		for (int a = 0; 2*c+a <= k; ++a)
			ans = (ans+solve(c, a))%mod;
	printf("%lld", ans);
	return 0;
}

sumary

考场上只切了一道……t2的正解在考场上大概想出来了,为求保险还是没有去写。

写到t3时脑子已经不在了,直接暴力tarjan骗40分

还是有很多问题的吧:

  • 码力还是太差了……
  • 考场上有正解还是要冲一冲(万一冲对了呢
  • 要充分转化题目使其变为好下手的
  • 有时候要考虑拆分题目,一个个条件解决