广东实验中学暑假训练-5
A
题意
通过删除一个字符串中的某些元素而不改变其余元素的顺序,可以派生出该字符 串的一个子序列。 例如,序列BDF 是ABCDEF 的子序列。 字符串的子字符串是该字符串的连续子序列。 例如,BCD 是ABCDEF 的子串。 你得到了两个字符串s1,s2 和另一个名为virus 的字符串。你的任务是找到s1 和s2 的最长公共子序列,同时不包含virus子字符串。
思路
最长公共子序列。考虑f[i][j][k]表示s1的前i位,s2的前j位且末段匹配到了virus的前k位最长公共子序列。
用kmp或者hash做字符串匹配均可。
转移:
- 若 \(s1_i=s2_j\),\(f(i-1,j-1,k) + 1 \to f(i,j,{\rm lenth}(k,s1_i))\)
- 若 \(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结点的集合有一个公共数字。
思路
L
题意
题意:给定n个节点的树,求满足条件的四元组(a,b,c,d)的数量:
- 1≤a<b≤n 1≤c<d≤n
- 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 的性质。
容易发现,这是一个“菊花图”,即由根节点延伸出若干条链。
可以用线段树维护每条链。分类讨论:
- 在 u 距离 d 范围内的节点都在同一条链上:线段树上直接区间修改。
- 在 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 节点连边,那么新树的直径是以下三者的最大值:
- x 树的直径
- y 树的直径
- 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,分类讨论:
- v 不在 x 的子树内时,此时 x 子树没有变化,因此直接对 x 子树进行操作即可。
- v 在 x 的子树内时,需要修改的是全树除去 x -> v 路径上的第一个节点为根的子树(不包括 x )
由于题目还要求 lca ,再次进行分类讨论:
- 若目前的根 v 不在子树 lca(x,y) 内,那么 lca(x,y) 不变
- 若 v 在路径 x -> y 上,那么 lca(x,y) = v
- 如果都不在,那么 lca(x,y) 就是 lca(x,v) 和 lca(y,v) 中深度最小(最靠近 v)的节点