广东实验中学暑假训练-5

发布时间 2023-08-18 16:48:56作者: __allenge

A

题意

通过删除一个字符串中的某些元素而不改变其余元素的顺序,可以派生出该字符 串的一个子序列。 例如,序列BDF 是ABCDEF 的子序列。 字符串的子字符串是该字符串的连续子序列。 例如,BCD 是ABCDEF 的子串。 你得到了两个字符串s1,s2 和另一个名为virus 的字符串。你的任务是找到s1 和s2 的最长公共子序列,同时不包含virus子字符串。

思路

最长公共子序列。考虑f[i][j][k]表示s1的前i位,s2的前j位且末段匹配到了virus的前k位最长公共子序列。

用kmp或者hash做字符串匹配均可。

转移:

  1. \(s1_i=s2_j\)\(f(i-1,j-1,k) + 1 \to f(i,j,{\rm lenth}(k,s1_i))\)
  2. \(s1_i \not= s2_j\)\(\max \{f(i-1,j,k),f(i,j-1,k)\} \to f(i,j,k)\)

\({\rm lenth}(k,x)\) 表示已经匹配了 virus 的前 k 位,下一个字母是 x 的情况下后缀能匹配到 virus 的最大长度(相当于 kmp 的 next 数组)。

B

题意

将s分割为若干段,要求每段必须包含子串t,求方案数。

思路

考虑朴素的dp

f[i]表示前i位分割为若干段(最后一段以i结尾)且每段含t的方案数。

\(f[i]=\sum f[k]且区间[k+1,i]需含有t\)

可以用 kmp 找到每个子串 t 出现的位置,然后 \(\Theta (n)\) 处理出所有位置 i 的最近的前一个 t 的开始位置。

前缀和优化即可,答案是 \(f(n)\)

C

题意

见洛谷

思路

较为常规的 dp

f[i][j][k] 表示填了 i 个字符,目前在节点 j 上且最后 k 个字符未匹配的方案数

转移:

\(f(i,j,k)\to \begin{cases}f(i+1,{\rm tr}[j,c],0) && {\rm {maxlenth}_{{tr}[j,c]}}>k \\ f(i+1,{\rm tr}[j,c],k+1) && \rm otherwise \end{cases}\)

AC 自动机上 dp 一般都是将 AC 自动机上的节点计入 dp 的状态,然后转移的时候在 AC 自动机上往下走一个节点。

D

题意

见洛谷

思路

数位dp + AC自动机

数位 dp 模板可以参考 windy 数。思路都是类似的,建议写成记忆化搜索的形式。

大概流程:

枚举当前数位上的数字 -> 是否有前导0,是否顶着上界 -> 记录状态返回

参考代码(windy):

int dfs(int n, int pre, bool limit, bool lead){// 状态 n limit lead 基本上是固定的
	int sum = 0;
	if(n == 0) return 1;//基本不变
	if(!limit and !lead and f[n][pre] != -1) return f[n][pre];//基本不变
	for(int i=0; i <= (limit?digit[n]:9); i++)
	    if(lead or (!lead and abs(i - pre) >= 2)) //不同题目的转移条件不同
	    	sum += dfs(n-1, i, limit&(i == digit[n]), lead&(i == 0));
	if(!limit and !lead) f[n][pre] = sum;//基本不变
	return sum;
}

完整代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 205, maxk = 505, mod = 1e9 + 7;
int digit[maxn], f[maxn][205][maxk], n, m, k, tr[maxn][21], fail[maxn], cnt[maxn], tot;
int dfs(int n, int now, int val, bool limit, bool lead){//数位 dp 模板
	int sum = 0;
	if(n == 0) return 1;
	if(!limit and !lead and f[n][now][val] != -1) return f[n][now][val];
	for(int i=0; i <= (limit ? digit[n] : m - 1); i++) {
		int nv = val;
		if(!lead or i > 0) nv += cnt[tr[now][i]];
		if(nv <= k)
			sum += dfs(n - 1, (lead and i == 0) ? now : tr[now][i], nv, limit & (i == digit[n]), lead & (i == 0)), sum %= mod;
	}
	if(!limit and !lead) f[n][now][val] = sum;
	return sum;
}
int solve(int num, int *x){
	for(int i = num; i >= 1; i --) digit[i] = x[num - i + 1];
	return dfs(num, 0, 0, 1, 1);
}
void insert(int lim, int *a, int val) {
	int p = 0;
	for(int i = 1; i <= lim; i ++)
		if(!tr[p][a[i]]) p = tr[p][a[i]] = ++ tot;
		else p = tr[p][a[i]];
	cnt[p] += val, cnt[p] %= mod;
}
void build() {
	queue<int> q;
	for(int i = 0; i < m; i ++)
		if(tr[0][i]) q.push(tr[0][i]);
	while(!q.empty()) {
		int u = q.front();
		q.pop();
		for(int i = 0; i < m; i ++){
			if(tr[u][i])
				fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
			else
				tr[u][i] = tr[fail[u]][i];
		}
		cnt[u] += cnt[fail[u]], cnt[u] %= mod;
	}
}
int tL, tR, L[maxn], R[maxn], a[maxn];
int main() {
	scanf("%d%d%d", &n, &m, &k);
	scanf("%d", &tL);
	for(int i = 1; i <= tL; i ++) scanf("%d", &L[i]);
	L[tL] --;
	for(int i = tL; i >= 1; i --) {
		if(L[i] < 0) {
			L[i] += m;
			L[i - 1] --;
		}
		else break;
	}
	scanf("%d", &tR);
	for(int i = 1; i <= tR; i ++) scanf("%d", &R[i]);
	for(int i = 1; i <= n; i ++) {
		int lenth, p;
		scanf("%d", &lenth);
		for(int j = 1; j <= lenth; j ++) scanf("%d", &a[j]);
		scanf("%d", &p);
		insert(lenth, a, p);
	}
	build();//AC 自动机模板
	memset(f, -1, sizeof(f));
//	for(int i = 0; i <= tot; i ++) cout<<cnt[i]<<" ";
	printf("%d", (solve(tR, R) - solve(tL, L) + mod) % mod);
	return 0;
}

E

题意

给定一个字符串。要求选取他的一个前缀(可以为空)和与该前缀不相交的一个后缀(可以为空)拼接成回文串,且该回文串长度最大。

思路

首先找到从前往后和从后往前的最大匹配长度,然后贴着前面或者后面找到最长的回文串。

贪心的正确性容易证明。

F

题意

给定一个\(n\times m\)的字符矩阵,请求出有多少个子矩阵在重排子矩阵每一行的字符后,使得子矩阵的每行每列都是回文串。

思路

看到范围可以猜测复杂度是\(n^3\)

因此可以考虑\(n^2\)枚举列[l,r],再在\(\Theta(n)\)以内判断哪些行是合法的。

判断横着的回文串的条件:出现奇数次的字符至多只出现了一次。

判断竖着的回文串的条件:对称的两行完全相同,用 hash 将比较一行字符串转化为数字。

横回文串可以用前缀和\(\Theta(1)\)判断,竖回文串可以用hash+manacher \(\Theta(n)\)判断

总复杂度\(\Theta(n^3)\)

G

题意

给定 n 个点的树,点有点权,求满足最大点权与最小点权之差小于等于 d 的连通子图数目。

思路

看到数据范围可以猜测复杂度为 \(\Theta (n ^2)\)

同时维护最大最小值不太好处理。因此可以固定最大值或最小值。

枚举每个节点,假定其为联通子图中的最小值。

然后将比该值小的以及差值大于 d 的节点全部删掉。

最后问题就变为了包含该节点的联通图数量。

简单的树形dp即可。

\(f(i)\) 表示包含节点 i 的连通块个数。

\(f(i)=\prod_{j\in son_i}(f(j) + 1)\) (每个子树 j 可以选或不选)

H

题意

一棵有根树,A,B轮流操作,给每个叶子分配权值(1~叶子节点数)的一个排列.每次每人可以任选一条边走,A希望最后取得的叶子结点的权值最大,B希望最后取得的叶子结点的权值最小,两人都绝对聪明,问若让A分配权值则最后取得的最大权值是多少,若让B分配权值则最后取得的最小权值是多少?

思路

首先考虑 A 最大能取到多少。

设 f(i) 表示当前节点 i (在双方足够聪明的前提下)能到达的叶子节点在 i 子树中是第几大的。

若 i 的深度为偶数(假设根节点深度为0),则 A 走,\(f(i)=\min \{f({\rm son}_i)\}\)

若 i 的深度为奇数,则 B 走,\(f(i)=\sum f({\rm son}_i)\)

B 的最小取值同理。

洛谷题面

I

题意

给一棵树,树的每个叶子节点上有权值,定义一颗树平衡:对于每一个结点 u 的子树都拥有相同的权值之和,问至少要减掉多少权值才能使树平衡。根的结点编号为 1。

思路

考虑如何使子树 x 平衡:将每个子节点都减去某个值,使得最后每个子节点的权值都相同。

假设 \(k_i\) 是能够保证 i 节点平衡的最小权值和(不包括 0)。

对于子节点 i,每次减去的值必须为 \(k_i\) 的倍数。

因此子树 x保持平衡的条件有:

1.每个子节点 i 权值相同
2.每个子节点 i 的权值和为 \({\rm lcm} (k_i)\) 的倍数(或 0)且相等。

dfs 一遍即可。

#include <bits/stdc++.h>
using namespace std;
#define int long long
inline int read() {
	int w = 0, f = 1; char ch = getchar();
	while(ch < '0' or ch > '9') {if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' and ch <= '9') w = w * 10 + ch - '0', ch = getchar();
	return w*f;
}
const int maxn = 1e6 + 5, INF = 1e8 + 7;
int a[maxn], head[maxn], Next[maxn << 1], ver[maxn << 1], tot;
void add(int x, int y) {
	ver[++ tot] = y, Next[tot] = head[x], head[x] = tot;
}
int n, son[maxn], w[maxn], Lcm[maxn];
int gcd(int x, int y) {
	if(!y) return x;
	return gcd(y, x % y);
}
void dfs(int x, int fa) {
	w[x] = a[x], Lcm[x] = 1;//Lcm 是该子树要保持平衡的最少需要的苹果数
	for(int i = head[x]; i; i = Next[i]) {
		if(ver[i] == fa) continue;
		dfs(ver[i], x);
		son[x] ++;
		Lcm[x] = Lcm[x] / gcd(Lcm[x], Lcm[ver[i]]) * Lcm[ver[i]];
		if(Lcm[x] > INF) Lcm[x] = INF; //题目中防止爆longlong的特判
		w[x] += w[ver[i]];//子树的苹果数(修改后平衡)
	}
	for(int i = head[x]; i; i = Next[i]) {
		if(ver[i] == fa) continue;
		w[x] = min(w[x], (int)(w[ver[i]] - w[ver[i]] % Lcm[x]));//每次尽可能多的留下子树中的苹果
	}
	if(son[x]) w[x] *= son[x], Lcm[x] *= son[x];//每个子树同时减去的苹果数应当相同
}
signed main() {
	scanf("%lld", &n);
	int total = 0;
	for(int i = 1; i <= n; i ++) a[i] = read(), total += a[i];
	for(int i = 1; i < n; i ++) {
		int x = read(), y = read();
		add(x, y), add(y, x);
	}
	dfs(1, 0);
	printf("%lld", total - w[1]);
	return 0;
}

J

题意

给定一个以 1 为根的 n 个结点的树,每个点上有一个字母(a-z),每个点的深度定义为该节点到 1 号结点路径上的点数。每次询问 a,b 查询以 a 为根的子树内深度为 b 的结点上的字母重新排列之后是否能构成回文串。

思路

暴力,dsu on tree

(好像有很多种做法)

dsu 模板:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 5;
inline int read(){
	int w = 0, f = 1; char ch = getchar();
	while(ch < '0' or ch > '9') {if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' and ch <= '9') w = w*10 + ch - '0', ch = getchar();
	return w*f;
}
vector< pair<int, int> > Q[maxn];
int N, M, f[maxn], head[maxn], ver[maxn<<1], Next[maxn<<1], tot;
void add(int x, int y){
	ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}
char str[maxn];
int siz[maxn], d[maxn], son[maxn];
void dfs1(int x, int deep){
	siz[x] = 1, d[x] = deep;
	for(int i=head[x]; i; i=Next[i]){
		int y = ver[i];
		dfs1(y, deep+1);
		siz[x] += siz[y];
		if(siz[son[x]] < siz[y]) son[x] = y;
	}
}
bool ans[maxn]; int Set[maxn];
void getans(int x, int p){
	Set[d[x]] ^= (1<<(str[x] - 'a'));
	for(int i=head[x]; i; i=Next[i]){
		int y = ver[i];
		if(y == p) continue;
		getans(y, p);
	}
}
void clear(int x){
	Set[d[x]] ^= (1<<(str[x] - 'a'));
	for(int i=head[x]; i; i=Next[i]){
		int y = ver[i];
		clear(y);
	}
}
void dfs2(int x){
	for(int i=head[x]; i; i=Next[i]){
		int y = ver[i];
		if(y == son[x]) continue;
		dfs2(y);//暴力统计每一个轻儿子答案
		clear(y);//清空轻儿子用的桶
	}
	if(son[x]) dfs2(son[x]);//暴力统计重儿子的答案
	getans(x, son[x]);//保留重儿子答案的同时把轻儿子内的答案也算上
	for(vector< pair<int, int> >::iterator it = Q[x].begin(); it != Q[x].end(); ++it){
		int S = Set[(*it).first];
		ans[(*it).second] = (S == (S&-S));//回答每一个离线下来的询问
	}
}
int main(){
	N = read(), M = read();
	for(int i=2; i<=N; i++) f[i] = read(), add(f[i], i);
	scanf("%s", str+1);
	for(int i=1; i<=M; i++){
		int a = read(), b = read();
		Q[a].push_back(make_pair(b, i));
	}
	dfs1(1, 1);
	dfs2(1);
	for(int i=1; i<=M; i++) printf(ans[i]?"Yes\n":"No\n");
	return 0;
}

K

题意

小象对一棵根节点编号为1,节点数为n的有根树进行m次操作。这棵树每个节点都有一个集合。第i次操作给出ai和bi,把i这个数字放入ai和bi这两个点为根的子树里的所有集合中。(包括ai和bi)在操作完后,输出ci表示有多少个结点(不包括i)的集合至少与i结点的集合有一个公共数字。

思路

见 Adam 学长的题解

L

题意

题意:给定n个节点的树,求满足条件的四元组(a,b,c,d)的数量:

  1. 1≤a<b≤n 1≤c<d≤n
  2. a到b和c到d的路径没有交点

思路

保证没有交点有点困难,因此可以反过来考虑,即找有交点路径的方案数。

枚举 (a, b) (c, d) 相交的节点 x (多个交点取深度最小的),考虑计算经过 x 的方案数:

1.两条路径都在 x 子树内,设方案数为 f(x)
2.一条路径在 x 内,另一条延申到了 x 子树外,设方案数为 g(x)

相交的总方案数就是 \(\sum f^2(i)+2f(i)g(i)\)

再用总方案数减去即可。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 8e4 + 5;
int head[maxn], Next[maxn<<1], ver[maxn<<1], tot;
void add(int x, int y){
	ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}
int N, siz[maxn];
long long f[maxn], g[maxn];
void dfs(int x, int fa){
	siz[x] = 1;
	for(int i=head[x]; i; i=Next[i]){
		int y = ver[i];
		if(y == fa) continue;
		dfs(y, x);
		f[x] += siz[y]*siz[x];
		siz[x] += siz[y];
	}
	g[x] = (N - siz[x])*siz[x];
}
int main(){
	scanf("%d", &N);
	for(int i=1; i<N; i++){
		int x ,y;
		scanf("%d%d", &x, &y);
		add(x, y); add(y, x);
	}
	dfs(1, 0);
	long long ans1 = 0ll, ans2 = 0ll;
	for(int i=1; i<=N; i++){
		ans1 += f[i];
		ans2 += f[i]*f[i] + f[i]*g[i]*2ll;
	}
	printf("%lld", ans1*ans1 - ans2);
	return 0;
}

M

题意

很久以前,有一棵神橡树(oak),上面有n个节点,从1~n编号,由n−1条边相连。它的根是1号节点。
这棵橡树每个点都有一个权值,你需要完成这两种操作:
1 u val 表示给u节点的权值增加val
2 u 表示查询u节点的权值
但是这不是普通的橡树,它是神橡树。
所以它还有个神奇的性质:当某个节点的权值增加val时,它的子节点权值都增加 −val,它子节点的子节点权值增加 −(−val)...... 如此一直进行到树的底部。

思路

看到修改子树中所有的权值,可以考虑树剖或者 dfs 序将树上问题转化为序列上问题。

本题只会修改子树,而且单点查询,因此用 dfs 序+树状数组即可。

值得注意的是,要按深度的奇偶性决定某节点权值增加或减少。

更加“古老”的学长题解

N

题意

给定一棵无边权的树,除了根节点以外的节点度数不超过 2,有两种操作:
1.(0 v x d)将距离 u 节点 d 距离之内的节点的值加上 x
2.(1 v)询问 u 节点的值

思路

考虑如何利用度数不超过 2 的性质。

容易发现,这是一个“菊花图”,即由根节点延伸出若干条链。

可以用线段树维护每条链。分类讨论:

  1. 在 u 距离 d 范围内的节点都在同一条链上:线段树上直接区间修改。
  2. 在 u 距离 d 范围内的节点跨越了根节点,延申到其他链上了:新开一个线段树来维护延申的深度,即在深度 \(d-{\rm deep}_u\) 以内区间加上 x 。

最后查询每个节点的权值就是两个线段树上的权值之和。

O

题意

给出一个由 n 个点,m 条边组成的森林,有 q 组询问:

1.给出点 x,输出点 x 所在的树的直径
2.给出点 x,y,(如果 x,y 在同一棵树中则忽略此操作)选择任意两点 u,v,使得 u 跟 x 在同一棵树中且 v 跟 y 在同一棵树中。将 u,v 之间连一条边,使得连边后的到的新树的直径最小。

思路

对于每棵树求出树的直径,然后考虑如何合并两棵树。

给 u,v 节点连边,那么新树的直径是以下三者的最大值:

  1. x 树的直径
  2. y 树的直径
  3. u 节点出发的最长链 + v 节点出发的最长链 + 1

因此只需要找到树中的一个节点,使得该节点出发的最长链最短。

不难发现,这个节点就是直径的中点。

用并查集维护树的合并即可。

P

题意

1.选择一个数 y 和两个节点 a, b 。沿着树的边走 a -> b 的最短路。每经过一条边 i 时,y 会变为 \(\lfloor \frac{y}{x_i} \rfloor\)
2. 选择第 i 条边,将其边权 \(x_i\) 改为 \(c_i\)\(c_i\in [1,x_i]\)

思路

由向下取整的性质可知:\(\left\lfloor\dfrac{\lfloor\frac{a}{b}\rfloor}{c} \right\rfloor = \left\lfloor\dfrac{a}{bc} \right\rfloor\)

因此一个很显然的做法是:树剖维护边权的乘积。

但是由于本题的特殊性质,还有更巧妙的做法:

已知 \(y\leq 10^{18}\) 因此在边权不为 1 的时候至多走过 60 条边 y 就会变为 0 。

同时,由于修改的边权是单调递减的,所以当一条边被修改为 1 后,边权就不会再改变。

经过边权为 1 的边时,相当于没有经过边,所以将这条边删掉也不会对答案产生影响(即将该边连接的两个节点合并)。

所以就有了十分简单的做法:用并查集将边权为 1 的点合并,再暴力跳边找 lca 。

Q

题意

有一棵 n 个节点的有根树,标号为 1∼n,你需要维护以下三种操作
1 v:给定一个点 v,将整颗树的根变为 v。
2 u v:给定两个点 u,v,将 lca(u,v) 为根的子树的所有点的点权都加上 x。
3 v:给定一个点 v,你需要回答以 v 所在的子树的所有点的权值和。

思路

如果没有换根操作,那么就是简单的子树的修改和查询,用树剖或者 dfs 序转为区间上问题解决。

假设树的根为 1,换的根为 v ,查询/修改的子树的根为 x,分类讨论:

  1. v 不在 x 的子树内时,此时 x 子树没有变化,因此直接对 x 子树进行操作即可。
  2. v 在 x 的子树内时,需要修改的是全树除去 x -> v 路径上的第一个节点为根的子树(不包括 x )

由于题目还要求 lca ,再次进行分类讨论:

  1. 若目前的根 v 不在子树 lca(x,y) 内,那么 lca(x,y) 不变
  2. 若 v 在路径 x -> y 上,那么 lca(x,y) = v
  3. 如果都不在,那么 lca(x,y) 就是 lca(x,v) 和 lca(y,v) 中深度最小(最靠近 v)的节点