动态规划题单4

91.[ARC114F] Permutation Division

Bob 的策略一定是按照开头从大到小排。
所以最后的答案一定不小于原来的排列。

所以我们要使得最后的排列 \(Q\) 和原排列 \(P\) 的 LCP 尽可能长。
也就是说最后分段的形式满足:(\(t_i\) 是每一段的开头)

  1. \(p_1=p_{t_1} > p_{t_2} > … > p_{t_m}\)
    不然的话,Bob 可以把后面的放到前面去。
    这些段就是 LCP 长度。
  2. \(p_{t_{m+1}},p_{t_{m+2}},…,p_{t_k} < p_{t_m}\)
    此时 Bob 会把 \(p_{t_{m+1}},p_{t_{m+2}},…,p_{t_k}\) 的 max 作为 \(q_{t_{m+1}}\)

考虑二分 LCP 的长度 \(len\),枚举 \(m\),对于每个 \(m\) 我肯定是求出 \(p_{t_m}\) 的最大值,这样后面的段的选择会变多,设 \([len+1,n]\)\(y\) 个数 < \(p_{t_m}\) 那么,如果 \(y+m\ge k\) 就是可行的,而 \(p_{t_{m+1}},p_{t_{m+2}},…,p_{t_k}\) 一定是选择最小的那几个。
于是问题变成求长度为 \(m\) 的下降子序列的末尾的最大值,这可以用 \(O(n\log n)\) 的 DP 来实现。

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=2e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,k,a[N],b[N],pos[N],g[N],d,cnt[N],t[N];
bool flag[N];
int check(int mid){  //返回最大的 m 
	if(mid==0) return 0;
	for(int i=1;i<=n;i++) g[i]=0;
	g[1]=-a[1],d=1;  //取 - 变成求最长上升子序列 
	for(int i=2;i<=mid;i++){
		int l=upper_bound(g+1,g+d+1,-a[i])-g;
		if(l==1) continue;
		g[l]=-a[i];
		d=max(d,l);
	}
	for(int i=1;i<=n;i++) cnt[i]=0;
	for(int i=mid+1;i<=n;i++) cnt[a[i]]++; 
	for(int i=1;i<=n;i++) cnt[i]+=cnt[i-1];  //cnt[i] 几位 [len+1,n] 中小于 i 的个数
	for(int m=d;m>=1;m--){
		if(m+cnt[-g[m]]>=k) return m;
	} 
	return 0;   
}
signed main(){
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	n=read(),k=read();
	for(int i=1;i<=n;i++) a[i]=read(),b[i]=a[i],pos[a[i]]=i;
	int l=1,r=n,mid,len=0;
	while(l<=r){
		mid=(l+r)>>1;
		if(check(mid)) len=mid,l=mid+1;
		else r=mid-1;
	} 
	int m=min(k,check(len));
	
	sort(b+len+1,b+n+1); 
	for(int i=len+1,j=1;j<=k-m;i++,j++) t[j]=pos[b[i]],flag[t[j]]=true;
	for(int i=1;i<=len;i++) printf("%d ",a[i]);
	for(int i=k-m;i>=1;i--){  //注意 Bob 对于后面那几段是从大到小放的 
		for(int j=t[i];j<=n&&(j==t[i] || !flag[j]);j++){
			printf("%d ",a[j]);
		}
	}
	puts("");
	return 0;
}

92.[SDOI2011] 消耗战

虚树上 dp 的裸题。

我们称那些有资源的点为关键点

首先这题单次询问有个 \(O(n)\) 的树形 DP。
即设 \(f_u\) 表示 \(u\) 号点与其子树中的所有关键点不连通的最小代价,那么枚举儿子 \(v\)

  1. \(v\) 不是关键点:\(f_u += \min(f_v,w(u,v))\)
  2. \(v\) 是关键点:\(f_u += w(u,v)\)

然后对所有关键点建出虚树在虚树上做这个 DP 就可以了。(应该不会有人看到 \(92\) 题了还不会虚树吧。)

code

#include<bits/stdc++.h>
#define int long long
#define PII pair<int,int>
#define fi first
#define se second 
using namespace std;
const int N=2.5e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,T,tot,head[N],to[N<<1],val[N<<1],Next[N<<1];
void add(int u,int v,int w){
	to[++tot]=v,val[tot]=w,Next[tot]=head[u],head[u]=tot;
}
int dfn[N],dep[N],fa[N][25],ming[N][25],num;
void dfs(int u){
	dfn[u]=++num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i],w=val[i];
		if(v==fa[u][0]) continue;
		fa[v][0]=u,ming[v][0]=w;
		dep[v]=dep[u]+1;
		dfs(v);
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}
int ask(int x,int y){
	int Min=LLONG_MAX;
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) Min=min(Min,ming[x][t]),x=fa[x][t];
	if(x==y) return Min;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) Min=min({Min,ming[x][t],ming[y][t]}),x=fa[x][t],y=fa[y][t];
	return min({Min,ming[x][0],ming[y][0]}); 	
}
int k,a[N],t[N],cnt;
bool flag[N];
vector<PII> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=0;
	t[++cnt]=1,t[++cnt]=a[k];
	for(int i=1;i<k;i++){
		t[++cnt]=a[i];
		t[++cnt]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+cnt+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=unique(t+1,t+cnt+1)-t-1;
	for(int i=1;i<=cnt;i++) G[t[i]].clear();
	for(int i=1;i<cnt;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back({t[i+1] , ask(t[i+1],lca)});   //单向边即可 
	}
}
int f[N];
void DP(int u){
	f[u]=0;
	for(PII e:G[u]){
		int v=e.fi,w=e.se;
		DP(v);
		if(flag[v]) f[u]+=w;
		else f[u]+=min(w,f[v]);
	}
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read(),w=read();
		add(u,v,w),add(v,u,w);
	}
	dep[1]=1;
	dfs(1);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
			ming[u][i]=min(ming[u][i-1] , ming[fa[u][i-1]][i-1]);
		}
	}
	T=read();
	while(T--){
		k=read();
		for(int i=1;i<=k;i++) a[i]=read(),flag[a[i]]=true;
		Build_Virtual_Tree();
		DP(1);
		printf("%lld\n",f[1]);
		for(int i=1;i<=k;i++) flag[a[i]]=false;
	}
	return 0;
}

93.[HEOI2014] 大工程

也是裸题。

建立虚树,然后简单树形 DP 求出:\(Size_i\) 表示 \(i\) 子树内关键点数量,\(f_i\) 表示 \(i\)\(i\) 子树内关键点的距离和,\(g_i\) 表示最小距离,\(h_i\) 表示最大距离。
注意到求最终答案时一条路径在有根树上肯定形如 \(x\to lca\to y\) 所以只要在 \(lca\) 处统计他即可。
所以求最终答案时只需要合并不同子树内的答案,具体的:
在用儿子 \(v\) 更新 \(u\) 的 DP 值之前, \(u\) 存的是 \(v\) 之前所有 \(v\) 的兄弟的信息,那么更新全局答案 \((sum,Min,Max)\):

  • \(f_u\times Size_v + w(u,v)\times Size_u\times Size_v + f_v\times Size_u \to sum\)
  • \(Min= \min(Min, g_u+w(u,v)+g_v)\)
  • \(Max= \max(Max, h_u+w(u,v)+h_v)\)

\(w(u,v)\) 表示 \(u,v\) 在虚树上的边权,在原树中就是 \(dep_v-dep_u\)

code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,T,tot,head[N],to[N<<1],val[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}
int dfn[N],dep[N],fa[N][25],num;
void dfs(int u){
	dfn[u]=++num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i],w=val[i];
		if(v==fa[u][0]) continue;
		fa[v][0]=u;
		dep[v]=dep[u]+1;
		dfs(v);
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}
int k,a[N],t[N],cnt;
bool flag[N];
vector<int> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=0;
	t[++cnt]=1,t[++cnt]=a[k];
	for(int i=1;i<k;i++){
		t[++cnt]=a[i];
		t[++cnt]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+cnt+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=unique(t+1,t+cnt+1)-t-1;
	for(int i=1;i<=cnt;i++) G[t[i]].clear();
	for(int i=1;i<cnt;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back(t[i+1]);   
	}
}
int sum,ming,maxn,Size[N],f[N],g[N],h[N];
void DP(int u){
	Size[u]=flag[u];
	f[u]=0;
	if(flag[u]) g[u]=h[u]=0;
	else g[u]=INT_MAX,h[u]=INT_MIN;
	for(int v:G[u]){
		DP(v);
		int w=dep[v]-dep[u];
		sum += f[u]*Size[v] + w*Size[u]*Size[v] + f[v]*Size[u];
		ming = min(ming , g[u]+w+g[v]);
		maxn = max(maxn, h[u]+w+h[v]);
		Size[u]+=Size[v];
		f[u]+=f[v]+Size[v]*w;
		g[u]=min(g[u],g[v]+w);
		h[u]=max(h[u],h[v]+w);
	}
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	dep[1]=1;
	dfs(1);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
		}
	}
	T=read();
	while(T--){
		k=read();
		for(int i=1;i<=k;i++) a[i]=read(),flag[a[i]]=true;
		Build_Virtual_Tree();
		sum=0,ming=INT_MAX,maxn=INT_MIN;
		DP(1);
		printf("%lld %lld %lld\n",sum,ming,maxn);
		for(int i=1;i<=k;i++) flag[a[i]]=false;
	}
	return 0;
}

94.[HNOI2014] 世界树

我也不太确定这个题到底该不该放进来,就当复习虚树吧。

虚树是显然的。
\(ans_i\) 表示关键点 \(i\) 最后的答案,称原树上离点 \(u\) 最近的关键点为 \(u\) 的对应点。
对于虚树上的点,可以换根 DP 求出 \(f_i\) 表示距离 \(i\) 号点最近的关键点的编号,我相信大家都会。
那对于不是虚树上的点怎么办?
会发现一个点不在虚树上只有两种情况:

  1. 原树上,他的这棵子树里一个关键点都没有。
  2. 他在虚树上的两个点在原树的那条链上。

对应到虚树上可以变为:

  1. 一个虚树节点 \(u\),他在原树上有一个儿子 \(v\),但 \(v\) 的子树中没有关键点,此时 \(v\) 子树中的所有点的对应点一定都是 \(f_u\),所以把 \(ans_{f_u}\) 加上 \(Size_v\)
  2. 虚树上的两个节点 \(x,y\) 其中 \(x\)\(y\) 的父亲,它们在原树上可能只是祖先后代关系,它们之间的链上还有一些点。
    假设其中有个点 \(c\),当然 \(c\) 上还可能挂了一些子树,显然这些子树的 \(f\) 值和 \(c\)\(f\) 值是一样。
    我们可以找到这条链上的一个分割点 \(u\),使得链上 \([u,y)\) 这段区间的对应点都是 \(f_y\)\((x,u)\) 这段区间的对应点都是 \(f_x\)

那具体怎么求呢?因为我们是不能去原树上遍历的。
对于 1. 中的情况:我们可以转换一下,求一个虚树点 \(u\) 所有不存在关键点的子树的大小的和,可以容斥用 \(Size_u\)\(-1\) (自己),再减去所有有关键点的子树的大小。
求有关键点的子树大小只需要遍历 \(u\) 虚树上的每个儿子 \(son\),在原树上倍增求出 \(son\) 在原树上的 \(dep_{son}-dep_u-1\) 级祖先 \(v\),那么 \(v\) 就是 \(u\) 在原树上的儿子,再让 \(Size_u\) 减去 \(Size_v\) 即可。
最后 \(Size_u \to ans_{f_u}\),注意 \(Size_u\) 还要还原。

对于 2. 中的情况:还是一样的对每个 \(x\),枚举虚树上的儿子 \(y\),然后还是倍增求出 \(x\) 在原树上含有 \(y\) 的那个儿子 \(v\),分界点 \(u\) 是可以二分/倍增求的。
求出 \(u,v\) 之后就可以更新答案了:\(Size_v-Size_u \to ans_{f_x}\)\(Size_u-Size_y \to ans_{f_y}\)
一定要注意不是用 \(Size_x-Size_u\) 去更新 \(ans_{f_x}\)

复杂度是 \(O(\sum m \times \log n)\) 或者 \(O(\sum m \times \log^2 n)\)(如果你用二分套倍增的话)。

code

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,T,tot,head[N],to[N<<1],val[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}
int dfn[N],dep[N],Size[N],fa[N][25],num;
void dfs(int u){
	dfn[u]=++num;
	Size[u]=1;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i],w=val[i];
		if(v==fa[u][0]) continue;
		fa[v][0]=u;
		dep[v]=dep[u]+1;
		dfs(v);
		Size[u]+=Size[v]; 
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}
int Get(int x,int len){
	for(int t=20;t>=0;t--) if(len>>t&1) x=fa[x][t];
	return x;
} 
int dist(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];}

int k,h[N],a[N],t[N],cnt;
bool flag[N];
vector<int> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=0;
	t[++cnt]=1,t[++cnt]=a[k];
	for(int i=1;i<k;i++){
		t[++cnt]=a[i];
		t[++cnt]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+cnt+1,[&](int x,int y){return dfn[x]<dfn[y];});
	cnt=unique(t+1,t+cnt+1)-t-1;
	for(int i=1;i<=cnt;i++) G[t[i]].clear();
	for(int i=1;i<cnt;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back(t[i+1]);   
	}
}

int f[N],ans[N];
void DP1(int u){   //先求子树中的关键点到 u 的距离最短的点 
	f[u]=(flag[u])?u:-1;
	for(int v:G[u]){
		DP1(v);
		if(f[u]==-1) f[u]=f[v];
		else if(dist(f[u],u) > dist(f[v],u)) f[u]=f[v];
		else if(dist(f[u],u) == dist(f[v],u)) f[u]=min(f[u],f[v]);
	}
}
void DP2(int u){ //换根 
	for(int v:G[u]){
		//用 u 取更新儿子,这里直接更新就可以,不需要考虑 f[u] 有没有可能在 v 子树的情况,因为这样一定不会用 f[u] 去更新 f[v] 
		if(f[v]==-1) f[v]=f[u];
		else if(dist(v,f[v]) > dist(v,f[u])) f[v]=f[u];
		else if(dist(v,f[v]) == dist(v,f[u])) f[v]=min(f[v],f[u]);
		DP2(v);
	}	
	ans[f[u]]++;
}
int check(int u,int x,int y){
	if(dist(u,x) < dist(u,y)) return x;
	else if(dist(u,x) > dist(u,y)) return y;
	else return min(x,y); 
}
void dfs1(int u){   //变量名稍有不同 
	int tmp=Size[u]-1;  //tmp 是处理第一种情况的 
	for(int v:G[u]){
		int x=Get(v,dep[v]-dep[u]-1);
		tmp-=Size[x];
		
		int l=1,r=dep[v]-dep[u]-1,mid,y=v;
		while(l<=r){
			mid=(l+r)>>1;
			if(check(Get(v,mid) , f[v] , f[u])==f[v]) l=mid+1,y=Get(v,mid);
			else r=mid-1;
		}
		ans[f[u]]+=Size[x]-Size[y];
		ans[f[v]]+=Size[y]-Size[v];
		
		dfs1(v);
	}
	ans[f[u]]+=tmp;
}
signed main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		add(u,v),add(v,u);
	}
	dep[1]=1;
	dfs(1);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			fa[u][i]=fa[fa[u][i-1]][i-1];
		}
	}
	T=read();
	while(T--){
		k=read();
		for(int i=1;i<=k;i++) h[i]=a[i]=read(),ans[a[i]]=0,flag[a[i]]=true;
		Build_Virtual_Tree();
		DP1(1);
		DP2(1);
		dfs1(1);  //求全局答案 
		for(int i=1;i<=k;i++) printf("%d ",ans[h[i]]);  //不是 a[i],因为 a[i] 已经被排序了 
		puts("");
		for(int i=1;i<=k;i++) flag[a[i]]=false;
	}
	return 0;
}

95.[HNOI/AHOI2018] 毒瘤

来一道不是那么简单的虚树题。
我保证这是最后一道虚树题了。

题解区还有 ddp广义串并联图 的做法,感兴趣的可以去看。

这个题就是求一个图的独立集个数。
对于一个树的情况,我们有 DP:

  1. \(f_{u,0}=\prod_{v \in son(u)} f_{v,0}+f_{v,1}\)
  2. \(f_{u,1}=\prod_{v \in son(u)} f_{v,0}\)

会发现非树边只有 \(m-n+1\le 11\) 条,对于这些非树边 \((u,v)\) 的两个端点状态只有三种:\((0,0),(0,1),(1,0)\)
而且会发现假设 \(u\) 在树上是 \(v\) 的祖先(无向图搜索树没有横插边),那么我们其实只关心 \(u\) 的状态是什么,即如果 \(u\) 钦定为 \(0\),那对 \(v\) 是没有影响的。
所以 \((0,0)\)\((0,1)\) 可以合并成一个情况,合起来之后就只需要钦定 \(u\) 的状态是什么。
暴力枚举是 \(O(2^{11} n)\)

会发现对 DP 产生影响的只有这 \(22\) 个点,考虑对他们建立虚树。
但虚树上明显不能用上面那个式子,我们来看虚树上的一条边 \((u,v)\) 在原树上对应的那条链:\(u \to x_1 \to x_2 \to … \to x_k \to v\)\(u\) 是链顶)。
对于这条链上的一个点 \(i\),如果定义 \(g_{i,0}=\prod f_{j,0} + f_{j,1},g_{i,1}=\prod f_{j,0}\),其中 \(j\) 是原树上 \(i\) 不在这条链上的儿子。
那么假设此时我们已经知道 \(v\)\(f\) 值了,那么我们尝试暴力推导出这条链上其他点的 \(f\) 值:
(\(1\))

  • \(f_{x_k,0} = g_{x_k,0} \times (f_{v,0} + f_{v,1})\)
  • \(f_{x_k,1} = g_{x_k,1} \times f_{v,0}\)

(\(2\))

  • \(f_{x_{k-1},0}\)
    \(= g_{x_{k-1},0} \times (f_{x_k,0} + f_{x_k,1})\)
    \(= g_{x_{k-1},0} \times (g_{x_k,0} \times (f_{v,0} + f_{v,1}) + g_{x_k,1} \times f_{v,0})\)
    \(= ( g_{x_{k-1},0} \times (g_{x_k,0} + g_{x_k,1}) ) \times f_{v,0} + (g_{x_{k-1},0} \times g_{x_k,0}) \times f_{v,1}\)
  • \(f_{x_{k-1},1}\)
    \(= g_{x_{k-1},1} \times f_{x_k,0}\)
    \(= g_{x_{k-1},1} \times g_{x_k,0} \times (f_{v,0} + f_{v,1})\)
    \(= g_{x_{k-1},1} \times g_{x_k,0} \times f_{v,0} + g_{x_{k-1},1} \times g_{x_k,0} \times f_{v,1}\)

(\(k+1\))

  • \(f_{u,0} = k_{v,0,0}\times f_{v,0} + k_{v,1,0}\times f_{v,1}\)
  • \(f_{u,1} = k_{v,0,1}\times f_{v,0} + k_{v,1,1}\times f_{v,1}\)

会发现不管这个虚树上每个点的状态钦定为什么,一条边 \((u,v)\) 的系数 \(k_{v,0/1,0/1}\) 是不变的。
我们只要算出每条边的系数就可以快速进行虚树上的树形 DP。
求系数其实也不用上面写出来的那么麻烦,程序里面你就模拟展开过程即可。
容易证明暴力对于每一条链 \((u,v)\) 去计算每个链上的点的 \(g\) 值和系数的总复杂度是 \(O(n)\) 的。

但此时会有个问题,就是 \(u\) 可能在虚树上不止一个儿子,即他可能还有一个虚树上的儿子 \(w\),那在处理 \((u,v)\)\(g_u\) 时,会需要用到 \(w\) 那棵子树的 DP 值,这样当去处理 \((u,w)\) 时就算重了。
处理方法也比较简单,把 \(g\) 的定义改一下:
\(g_{i,0}=\prod f_{j,0} + f_{j,1},g_{i,1}=\prod f_{j,0}\),其中 \(j\)\(i\) 在原树上的儿子,且 \(j\) 这棵子树里不存在虚树上的点。
容易发现那些 \(x_1,x_2,…,x_k\)\(g\) 值还是不变的。
但注意的是此时在算 \(k_{v,0/1,0/1}\) 时就不要再把 \(u\)\(g\) 值算进去了,即此时 \(k_{v,0,0}\times f_{v,0} + k_{v,1,0}\times f_{v,1}\) 算出来的并不是 \(f_{u,0}\) 而是 \(f_{x_k,0}\)

那么在虚树上 DP 时,\(f_{u,0}\) 的值就是:

\[\begin{aligned} f_{u,0} &= g_{u,0} \times \prod(f_{x,k,0} + f_{x_k,1}) \\ &= g_{u,0} \times \prod(k_{v,0,0}\times f_{v,0} + k_{v,1,0}\times f_{v,1} +k_{v,0,1}\times f_{v,0} + k_{v,1,1}\times f_{v,1}) \\ \end{aligned} \]

复杂度是:\(O(2^K K)\)\(K=m-n+1\)

code

#include<bits/stdc++.h>
#define PII pair<int,int>
#define int long long 
#define fi first
#define se second
using namespace std;
const int N=1e5+15,mod=998244353;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
int n,m,tot,head[N],to[N<<1],Next[N<<1];
void add(int u,int v){
	to[++tot]=v,Next[tot]=head[u],head[u]=tot;
}

int dfn[N],dep[N],fa[N][25],num,cnt;
PII e[N];  //存非树边 
void dfs1(int u){    
	dfn[u]=++num;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa[u][0]) continue;
		if(!dfn[v]){
			fa[v][0]=u,dep[v]=dep[u]+1;
			dfs1(v);
		} 
		else if(dfn[v]<dfn[u]) e[++cnt]={v,u};
	}
}
int LCA(int x,int y){
	if(dep[x]<dep[y]) swap(x,y);
	for(int t=20;t>=0;t--)
		if(dep[fa[x][t]]>=dep[y]) x=fa[x][t];
	if(x==y) return x;
	for(int t=20;t>=0;t--)
		if(fa[x][t] != fa[y][t]) x=fa[x][t],y=fa[y][t];
	return fa[x][0]; 
}

int k,h[N],a[N],t[N],c;
bool flag[N];
vector<int> G[N];
void Build_Virtual_Tree(){
	sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	k=unique(a+1,a+k+1)-a-1;
	c=0;
	t[++c]=1;
	if(k) t[++c]=a[k];
	for(int i=1;i<k;i++){
		t[++c]=a[i];
		t[++c]=LCA(a[i],a[i+1]);
	}
	sort(t+1,t+c+1,[&](int x,int y){return dfn[x]<dfn[y];});
	c=unique(t+1,t+c+1)-t-1;
	for(int i=1;i<=c;i++) flag[t[i]]=true,G[t[i]].clear();

	for(int i=1;i<c;i++){
		int lca=LCA(t[i],t[i+1]);
		G[lca].push_back(t[i+1]);   
	}
}

void Init(){
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read();
		add(u,v),add(v,u); 
	} 
	dep[1]=1;
	dfs1(1);
	for(int i=1;i<=20;i++)
		for(int u=1;u<=n;u++)
			fa[u][i]=fa[fa[u][i-1]][i-1];
	for(int i=1;i<=cnt;i++) a[++k]=e[i].fi,a[++k]=e[i].se;
	
	Build_Virtual_Tree(); 
}



int f[N][2],g[N][2];
bool stater[N];  //子树里是否有虚树上的点 
void dfs2(int u){    //预处理 g 数组 
	stater[u]=flag[u];
	f[u][0]=f[u][1]=g[u][0]=g[u][1]=1;
	for(int i=head[u];i;i=Next[i]){
		int v=to[i];
		if(v==fa[u][0] || fa[v][0]!=u) continue; 
		dfs2(v);
		stater[u]|=stater[v];
		( f[u][0] *= (f[v][0] + f[v][1]) % mod) %= mod;
		( f[u][1] *= f[v][0] ) %= mod;
		if(!stater[v]){
			( g[u][0] *= (f[v][0] + f[v][1]) % mod) %= mod;
			( g[u][1] *= f[v][0] ) %= mod;
		} 
	}
}

int K[N][2][2],tmp[2][2];  //系数,因为上面小写 k 用过了,所以大写 
void solve(int u,int v){   //预处理 K[v][0/1][0/1] 
	K[v][0][0]=K[v][1][1]=1;
	int x=fa[v][0];
	while(x!=u){
		tmp[0][0]=K[v][0][0],tmp[0][1]=K[v][0][1],tmp[1][0]=K[v][1][0],tmp[1][1]=K[v][1][1];
		K[v][0][0] = ( tmp[0][0] + tmp[0][1] ) % mod * g[x][0] % mod;
		K[v][1][0] = ( tmp[1][0] + tmp[1][1] ) % mod * g[x][0] % mod;
		K[v][0][1] = tmp[0][0] * g[x][1] % mod;
		K[v][1][1] = tmp[1][0] * g[x][1] % mod; 
		x=fa[x][0];
	}
}

void Get_DP(){
	dfs2(1);
	for(int u=1;u<=n;u++)
		for(int v:G[u]) solve(u,v);
}



void DP(int u){
	(f[u][0]*=g[u][0])%=mod,(f[u][1]*=g[u][1])%=mod;
	for(int v:G[u]){
		DP(v);
		( f[u][0] *= ( (K[v][0][0] * f[v][0] % mod + K[v][1][0] * f[v][1] % mod) % mod + ( K[v][0][1] * f[v][0] % mod + K[v][1][1] * f[v][1] % mod) % mod ) % mod ) %= mod;
		( f[u][1] *= (K[v][0][0] * f[v][0] % mod + K[v][1][0] * f[v][1] % mod) % mod ) %= mod;
	}
}
void work(){
	int ans=0;
	for(int S=0;S<(1<<cnt);S++){
		for(int i=1;i<=c;i++) f[t[i]][0]=f[t[i]][1]=1;
		for(int i=1;i<=cnt;i++)
			if(S>>(i-1)&1) f[e[i].fi][0]=0,f[e[i].se][1]=0;
			else f[e[i].fi][1]=0;
		DP(1);
		( ans += (f[1][0] + f[1][1]) % mod ) %= mod;
	}
	printf("%lld\n",ans);
}
signed main(){
	Init();
	Get_DP(); 
	work();
	return 0;
}

96.[HNOI2004] L 语言

先看朴素的 DP,设 \(f_i\) 表示前缀 \(i\) 可不可以被理解,转移是:\(f_j \to f_i\),需要满足 \(s[j+1,i]\) 是一个单词。

考虑对字典里的模式串建出 AC 自动机。
如果 \(t\) 的前缀 \(i\) 在 fail 树上代表点 \(u\),并且他的一个祖先 \(v\) 是一个模式串的终止节点。
\(v\) 代表的模式串长度为 \(len\),则 \(i\) 的长度为 \(len\) 的后缀就是一个模式串。
也就意味着 \(f_{i-len}\) 可以转移到 \(f_i\)
如果在 fail 树上暴力地跳,并挨个判断每个祖先,那么是 \(O(m|s||t|)\) 的。

注意到 \(|s|\le 20\),也就是说所有以 \(u\) 结尾的模式串的长度是可以状压的,我们记在 \(g\) 数组里面,即 \(g_u\) 的第 \(k\) 位是 \(1\) 代表 \(u\) 结尾有一个长度为 \(k\) 的模式串。
又因为能转移到 \(f_i\)\(f_j\) 必定满足 \(j\ge i-|s|\),所以我们也可以对 \(f\) 数组状压,记在 \(h\) 数组里,即如果 \(h_i\) 的第 \(k\) 位是 \(1\) 那么代表 \(f_{i-k}=true\)
所以 \(f_i = [(g_u | h_i) \ne 0]\)\(u\) 是前缀 \(i\) 在 fail 树上代表的点。
其中 \(g\) 可以预处理,\(h\) 数组在转移的时候维护就可以了。

复杂度是:\(O(\sum |s| + \sum |t|)\)

code

#include<bits/stdc++.h>
using namespace std;
const int N=500+5,M=2e6+5;
inline int read(){
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}
string s;
int n,T,tot,t[N][30],len[N],flag[N];
void insert(string s){  
	int p=0,dep=0;
	for(int i=0;i<s.size();i++){
		int ch=s[i]-'a';
		if(!t[p][ch]) t[p][ch]=++tot;
		p=t[p][ch],++dep;
	}
	flag[p]=1;
	len[p]=dep;
}

int fail[N];
vector<int> G[N];
void getfail(){
	queue<int> q;
	for(int i=0;i<26;i++){
		if(t[0][i]) q.push(t[0][i]);
	}
	while(q.size()){
		int u=q.front(); q.pop();
		for(int i=0;i<26;i++){
			if(t[u][i])
				fail[t[u][i]]=t[fail[u]][i],q.push(t[u][i]);
			else
				t[u][i]=t[fail[u]][i];
		}
	}
	for(int i=1;i<=tot;i++) G[fail[i]].push_back(i);
}

int g[M];
bool f[M];
void dfs(int u){
	for(int v:G[u]){
		g[v]=g[u] | (flag[v] * (1<<len[v])); 
		dfs(v);
	} 
}

void Init(){
	memset(f,false,sizeof f);
} 
signed main(){
	ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
	cin>>n>>T;
	for(int i=1;i<=n;i++){
		cin>>s;
		insert(s);
	}
	getfail();
	dfs(0);
	while(T--){
		Init();
		cin>>s; s=' '+s;
		int p=0,h=1,ans=0;
		for(int i=1;i<s.size();i++){
			p=t[p][s[i]-'a'];
			h<<=1;
			if((h&g[p])!=0) f[i]=true,ans=max(ans,i);
			h|=f[i];
		}
		printf("%d\n",ans);
	}
	return 0;
}

来源链接:https://www.cnblogs.com/FloatingLife/p/18808309

请登录后发表评论

    没有回复内容