[笔记]树形dp - 1/4(节点选择类)

树形dp,是一种建立在树形结构上的dp,因此dfs一般是实现它的通用手段。
是一种很美的动态规划呢。

P1352 没有上司的舞会

P1352 没有上司的舞会

在一棵树中,找到若干个互相独立(即互相没有边直接相连)的点,使它们的权值和最大。


我们发现,间隔选择的方法(只选深度为奇数/偶数的点)是不可行的。一个很简单的反例是这棵树是一条链:10 <-> 3 <-> 3 <-> 10,显然选择\(1,4\)才是正确的。

那么我们该怎么做呢?我们可以dfs遍历这棵树,对于一个节点,我们考虑两种选择情况:

  • 选当前节点:那么子节点就不能选。当前权值为所有子节点不选状态下的答案和。
  • 不选当前节点:那么子节点可以选,也可以不选。当前答案为所有子节点两种状态下的最大值之和。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,happy[6010];
vector<int> ch[6010];
bool b[6010];
int mem[6010][2];
int dfs(int pos,bool is){
	if(mem[pos][is]) return mem[pos][is];
	int ans=0,len=ch[pos].size();
	if(is){//如果上司去,则员工必须不去
		for(int i=0;i<len;i++) ans+=dfs(ch[pos][i],0);
		ans+=happy[pos]*(happy[pos]>0);
	}else{//如果上司不去,则员工有两种选择
		for(int i=0;i<len;i++) ans+=max(dfs(ch[pos][i],0),dfs(ch[pos][i],1));
	}
	return mem[pos][is]=ans;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>happy[i];
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		b[x]=1;
		ch[y].emplace_back(x);
	}
	for(int i=1;i<=n;i++){
		if(!b[i]){
			cout<<max(dfs(i,0),dfs(i,1));
			break;
		}
	}
	return 0;
}

UVA1292 Strategic game

UVA1292 Strategic game

在一棵树中,找到若干个点,每个点放置\(1\)个士兵,每个士兵可以看守所在点邻接的所有边,现在我们想知道:要看守这棵树的所有边,最少需要多少士兵。


对于每个节点,考虑其选择情况:

  • 该节点放士兵:那么子节点可以放,也可以不放。取每个子节点放/不放的最小值求和即可。
  • 该节点不放士兵:那么子节点必须全放。取每个子节点放的和即可。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n;
vector<int> G[1510];
int f[1510][2];
bool vis[1510];
void dfs(int pos){
	f[pos][1]=1;
	for(auto i:G[pos]){
		dfs(i);
		f[pos][0]+=f[i][1];//如果当前不选,那么子节点必须全选
		f[pos][1]+=min(f[i][0],f[i][1]);//如果当前选,则选最小
	}
}
int main(){
	while(~scanf("%d",&n)){
		memset(f,0,sizeof f);
		memset(vis,0,sizeof vis);
		for(int i=1;i<=n;i++) G[i].clear();
		for(int i=1;i<=n;i++){
			int pos,m,num;
			scanf("%d:(%d)",&pos,&m);
			pos++;
			for(int j=1;j<=m;j++){
				cin>>num;
				num++;
				G[pos].emplace_back(num);
				vis[num]=1;
			}
		}
		int awa=-1;
		for(int i=1;i<=n;i++){
			if(!vis[i]){
				awa=i;
				break;
			}
		}
		dfs(awa);
		cout<<min(f[awa][0],f[awa][1])<<endl;
	}
	return 0;
}

P1122 最大子树和

P1122 最大子树和

在一棵树中选择一块联通分量,让其点权和最大。


\(f[i]\)表示以\(i\)为根节点子树的答案。显然它的值就是子节点答案中,非负答案的和。

最后遍历\(f\)求出最大值即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,v[16010];
vector<int> G[16010];
bool vis[16010];
int f[16010];
void dfs(int pos){
	vis[pos]=1;
	f[pos]=v[pos];
	for(auto i:G[pos]){
		if(vis[i]) continue;
		dfs(i);
		if(f[i]>0) f[pos]+=f[i];
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>v[i];
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1);
	int ans=INT_MIN;
	for(int i=1;i<=n;i++) ans=max(ans,f[i]);
	cout<<ans;
	return 0;
}

USACO08JAN Cell Phone Network G

P2899 [USACO08JAN] Cell Phone Network G

在一棵树中,选若干个点放置信号站。对于每一个信号站,它可以让自己和邻接的节点都覆盖信号。请问最少需要放置多少个信号站,才能让所有节点都覆盖信号?


其实这个题是可以用贪心做的,思路很简单。

我们考虑一个叶子节点,要么由它自己覆盖,要么由和它距离为\(1\)的点覆盖。
在这些选择中,显然越靠上的点越优。所以我们不断暴力选未被覆盖点的父节点建信号站即可。

时间复杂度\(O(n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
vector<int> G[10010];
bool is[10010];//is记录是否覆盖信号
int n,fa[10010],ans;//fa记录父节点
void dfs(int pos){
	for(int i:G[pos]){
		if(i==fa[pos]) continue;
		fa[i]=pos;
		dfs(i);
	}
	if(!is[pos]){
		is[fa[pos]]=1;
		ans++;
		for(int i:G[fa[pos]]) is[i]=1;
	}
}
int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1);
	cout<<ans;
	return 0;
}

如果我们从树形dp的角度来思考,也不困难。
对于一个节点\(pos\),有三个状态:

  • \(f[pos][0]\)至少\(pos\)向上\(1\)层之下都覆盖信号的答案。
  • \(f[pos][1]\)至少\(pos\)自己及之下覆盖信号的答案。
  • \(f[pos][2]\)至少\(pos\)向下\(1\)层之下都覆盖信号的答案。

注意:这\(3\)种状态是包含关系,所以\(f[pos][0]\ge f[pos][1]\ge f[pos][2]\)

初始值:
对于叶子结点\(i\)\(f[i][0]=1,f[i][1]=1,f[i][2]=0\)
对于非叶子节点\(i\)\(f[i][0]=f[i][1]=f[i][2]=0\)

转移:
\(f[pos][0]=\sum\limits_{i是pos子节点}f[i][2]+1\)
\(f[pos][1]=\min(\quad\min\limits_{i是pos子节点}(f[i][0]+\sum\limits_{j是pos子节点,j\ne i}f[j][1])\quad,f[pos][0])\)
\(f[pos][2]=\min(\sum\limits_{i是pos子节点}f[i][1],f[pos][1])\)

解释:

  • 如果要覆盖上一层,则\(pos\)显然必须放,那么要让子节点尽可能小,应该让它们都为状态\(2\)。别忘了自己放需要额外\(+1\)
  • 如果要覆盖自己,那么子节点有一个是状态\(0\),这样\(pos\)就被覆盖了,其他子节点想要尽可能优,应当让它们都为状态\(1\)。注意要与\(f[pos][0]\)再取一次\(\min\)(一个小trick:在代码实现中,我们可以先全累加\(f[son][1]\),最后找一个\(f[son][0]-f[son][1]\)的最小值额外累加进\(f[pos][1]\)即可)。
  • 如果只需要覆盖下一层,那么子节点全为状态\(1\)是最好的。注意要与\(f[pos][1]\)再取一次\(\min\)

大家可能还会有疑问:比较\(f[pos][1]\)\(f[pos][2]\)的转移,我发现\(\min\limits_{i是pos子节点}(f[i][0]+\sum\limits_{j是pos子节点,j\ne i}f[j][1])\)显然\(\ge \sum\limits_{i是pos子节点}f[i][1]\),那为什么求\(f[pos][2]\)时还要与\(f[pos][1]\)取最小值呢?

因为在求\(f[pos][1]\)时,不能忽略的是我们还与\(f[pos][0]\)取了一次最小值,所以\(f[pos][1]\)是有可能\(<\sum\limits_{i是pos子节点}f[i][1]\)的,因此需要再比较一次。

时间复杂度为\(O(n)\)

注意双向存图。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,f[10010][3],vis[10010];
vector<int> G[10010];
void dfs(int pos){
	vis[pos]=1;
	bool isleaf=1;
	for(int i:G[pos]) if(!vis[i]) dfs(i),isleaf=0;
	if(isleaf) f[pos][0]=1,f[pos][1]=1,f[pos][2]=0;
	else{
		f[pos][0]=1;
		for(int i:G[pos]){
			if(vis[i]) continue;
			f[pos][0]+=f[i][2];
		}
		int minn=INT_MAX;
		for(int i:G[pos]){
			if(vis[i]) continue;
			f[pos][1]+=f[i][1];
			minn=min(minn,f[i][0]-f[i][1]);
		}
		f[pos][1]+=minn;
		f[pos][1]=min(f[pos][1],f[pos][0]);
		for(int i:G[pos]){
			if(vis[i]) continue;
			f[pos][2]+=f[i][1];
		}
		f[pos][2]=min(f[pos][2],f[pos][1]);
	}
	vis[pos]=0;
}
int main(){
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1);
	cout<<f[1][1];
	return 0;
}

HNOI2003 消防局的设立

P2279 [HNOI2003] 消防局的设立

此题和上道题很像,只不过建立一个消防站可以扑灭距离在\(2\)以内的火灾,而非上一题的\(1\)

这次我们需要设\(5\)个状态。

  • \(f[pos][0]\)至少\(pos\)向上\(2\)层之下都覆盖信号的答案。
  • \(f[pos][1]\)至少\(pos\)向上\(1\)层之下都覆盖信号的答案。
  • \(f[pos][2]\)至少\(pos\)自己及之下覆盖信号的答案。
  • \(f[pos][3]\)至少\(pos\)向下\(1\)层之下都覆盖信号的答案。
  • \(f[pos][4]\)至少\(pos\)向下\(2\)层之下都覆盖信号的答案。

同样地,\(f[pos][0]\ge f[pos][1]\ge f[pos][2]\ge f[pos][3]\ge f[pos][4]\)

初始值:
对于叶子结点\(i\)\(f[i][0]=1,f[i][1]=1,f[i][2]=1,f[i][3]=0,f[i][4]=0\)
对于非叶子节点\(i\)\(f[i][0]=f[i][1]=f[i][2]=f[i][3]=f[i][4]=0\)

转移:
\(f[pos][0]=\sum\limits_{i是pos子节点}f[i][4]+1\)
\(f[pos][1]=\min(\quad\min\limits_{i是pos子节点}(f[i][0]+\sum\limits_{j是pos子节点,j\ne i}f[j][3])\quad,f[pos][0])\)
\(f[pos][2]=\min(\quad\min\limits_{i是pos子节点}(f[i][1]+\sum\limits_{j是pos子节点,j\ne i}f[j][2])\quad,f[pos][1])\)
\(f[pos][3]=\min(\sum\limits_{i是pos子节点}f[i][2],f[pos][2])\)
\(f[pos][4]=\min(\sum\limits_{i是pos子节点}f[i][3],f[pos][3])\)

和上一题原理类似。时间复杂度\(O(n)\)

不用双向存图。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,f[1010][5];
vector<int> G[1010];
void dfs(int pos){
	if(G[pos].empty()){
		f[pos][0]=1,f[pos][1]=1,f[pos][2]=1;
		return;
	}
	for(int i:G[pos]) dfs(i);
	f[pos][0]=1;
	int min1=INT_MAX,min2=INT_MAX;
	for(int i:G[pos]){
		f[pos][0]+=f[i][4];
		f[pos][1]+=f[i][3];
		f[pos][2]+=f[i][2];
		f[pos][3]+=f[i][2];
		f[pos][4]+=f[i][3];
		min1=min(min1,f[i][0]-f[i][3]);
		min2=min(min2,f[i][1]-f[i][2]);
	}
	f[pos][1]+=min1,f[pos][2]+=min2;
	f[pos][1]=min(f[pos][1],f[pos][0]);
	f[pos][2]=min(f[pos][2],f[pos][1]);
	f[pos][3]=min(f[pos][3],f[pos][2]);
	f[pos][4]=min(f[pos][4],f[pos][3]);
}
int main(){
	cin>>n;
	for(int i=2;i<=n;i++){
		int u;
		cin>>u;
		G[u].emplace_back(i);
	}
	dfs(1);
	cout<<f[1][2];
	return 0;
}

虽然4202年了火星上还没有基地就是了

这道题的贪心代码就不提供了,因为下面还有覆盖距离为\(k\)的题目。

P3942 将军令

P3942 将军令

在一个树形结构中选若干个点驻扎军队,每个军队可以看守到所在节点距离不超过\(k\)的节点。请问最少需要选多少个节点驻扎军队,才能看守所有节点?

上面题的加强版。

贪心解法

先说下贪心(from 题解 P3942 【将军令】无预处理O(n) by Accoty_AM)。仍然是不断找最低的未被覆盖的节点,然后在它的第\(k\)代祖先处设置军队。

代码实现的话,我们定义\(f[i][0]\)\(i\)到以\(i\)为根的子树中,驻扎军队的节点的最小距离;\(f[i][1]\)表示\(i\)到以\(i\)为根的子树中,未被控制的节点的最大距离。

初始状态下\(f[i][0]=\infty,f[i][1]=0\)

对于一个节点\(pos\),我们搜索它所有的子节点。统计过程中\(f[pos][0]=\min(f[pos][0],f[i][0]+1),f[pos][1]=\max(f[pos][1],f[i][1]+1)\)

  • 首先,如果\(f[pos][1]+f[pos][0]\le k\),则说明该节点不用放军队就被子节点覆盖了。那么\(f[pos][1]=-1\)(如果将\(f[pos][1]\)设为\(-1\),那么在它的父节点计算距离时就会加成\(0\),也就相当于不参与比较了)。

  • 否则,如果我们发现\(f[pos][1]\)增加到了\(k\),说明需要在\(pos\)位置设置军队。那么\(ans++,f[pos][0]=0,f[pos][1]=-1\)
    解释:如果最大距离没到\(k\)就设置,会使答案不够优(因为我们要让军队尽可能靠上,这样才有希望覆盖更多节点)。而如果最大距离超过\(k\)才设置,会有节点覆盖不到。

最后,在主函数中我们需要特判,如果\(f[1][1]\ne -1\),说明根节点\(1\)还没被覆盖,则需要单独在根节点\(1\)设置一个军队,\(ans++\)

时间复杂度\(O(n)\),注意双向加边。

点击查看代码
#include<bits/stdc++.h>
#define inf 0x3f3f3f3f
using namespace std;
int n,k,t,ans,f[100010][2];
vector<int> G[100010];
void dfs(int pos,int fa){
	f[pos][0]=inf,f[pos][1]=0;
	for(int i:G[pos]){
		if(i==fa) continue;
		dfs(i,pos);
		f[pos][1]=max(f[pos][1],f[i][1]+1);
		f[pos][0]=min(f[pos][0],f[i][0]+1);
	}
	if(f[pos][1]+f[pos][0]<=k) f[pos][1]=-1;
	else if(f[pos][1]==k){
		ans++;
		f[pos][0]=0;
		f[pos][1]=-1;
	}
}
int main(){
	cin>>n>>k>>t;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1,0);
	if(~f[1][1]) ans++;
	cout<<ans;
	return 0;
}

dp解法

接下来我们考虑dp解法。

受之前做法的启发,我们定义\(f[pos][t]\)为根节点为\(pos\),状态为\(t\)的答案。

  • \(-k\le t<0\)至少\(pos\)向上\(-t\)层以下都覆盖。
  • \(t=0\)至少\(pos\)及以下都覆盖。
  • \(0<t\le k\)至少\(pos\)向下\(t\)层以下都覆盖。

根据定义,有\(f[-k]\ge…\ge f[-2]\ge f[-1]\ge f[0]\ge f[1]\ge f[2]\ge…\ge f[k]\)

\(t\)在写代码时会加一个\(k\),所以不必担心越界。

初始值:对于叶子节点\(i\):对于\(-k\le t\le 0\)\(f[i][t]=1\);对于\(0<t\le k\)\(f[i][t]=0\)
对于非叶子节点\(i\):对于\(-k\le t\le k\)\(f[i][t]=0\)

接下来就是转移部分了。枚举\(t\),递推式从前两道题找规律即可,不过还是语言说明一下。

  • 对于\(t=-k\)\(f[pos][t]=\sum\limits_{i是pos子节点}f[i][k]+1\)
    解释:必须要\(pos\)亲自出马设置军队,所以需要额外\(+1\)。因为\(pos\)已经放了,所以子节点全部为\(k\)是最优的。
  • 对于\(-k<t\le 0\)\(f[pos][t]=\min(\quad\min\limits_{i是pos子节点}(f[i][t-1]+\sum\limits_{j是pos子节点,j\ne i}f[j][-t])\quad,f[pos][t-1])\)
    解释:如果你需要覆盖\(pos\)往上\(x\)层,那么需要有\(1\)个特殊的子节点去覆盖自己上面\(x+1\)层,这就是\(t-1\)的含义。而这个特殊的子节点覆盖了自己上面\(x+1\)层后,其他子节点就可以稍微摸一下鱼了,因为它们只需要让自己往下\(x\)层以下都被覆盖即可,其他已经被这个特殊的子节点覆盖了,这就是\(-t\)的含义。对了,别忘了要和上一次推得的状态\(f[pos][t-1]\)\(\min\)(这些内容可以结合前两道题的转移来理解)。
  • 对于\(0<t\le k\)\(f[pos][t]=\min(\sum\limits_{i是pos子节点}f[i][t-1],f[pos][t-1])\)
    解释:如果你需要覆盖\(pos\)往下\(x\)层,那么子节点需要全部覆盖自己往下的\(x-1\)层。注意也要和\(f[pos][t-1]\)\(\min\)

实现细节:

  • \(f\)第二维要开\(2\)倍空间,因为我们要用\(2k+1\)个状态。
  • 需要双向存图。
  • 别忘了加上偏移量\(k\)
点击查看代码
#include<bits/stdc++.h>
#define o k
//为防止影响阅读代码,用o来代替k表示偏移量
using namespace std;
int n,k,t,f[100010][50];
int minn[30];//对应上面提到的小trick
vector<int> G[100010];
void dfs(int pos,int fa){
	bool isleaf=1;
	for(int i:G[pos]) if(i!=fa) isleaf=0,dfs(i,pos);
	if(isleaf){
		for(int i=-k;i<=0;i++) f[pos][i+o]=1;
	}else{
		memset(minn,127,sizeof minn);
		f[pos][-k+o]=1;
		for(int i:G[pos]){
			if(i==fa) continue;
			f[pos][-k+o]+=f[i][k+o];
			for(int t=-k+1;t<=0;t++)
				f[pos][t+o]+=f[i][-t+o],
				minn[t+o]=min(minn[t+o],f[i][t-1+o]-f[i][-t+o]);
			for(int t=1;t<=k;t++)
				f[pos][t+o]+=f[i][t-1+o];
		}
		for(int t=-k+1;t<=0;t++) f[pos][t+o]+=minn[t+o];
		for(int t=-k+1;t<=k;t++)
			f[pos][t+o]=min(f[pos][t+o],f[pos][t-1+o]);
	}
}
int main(){
	cin>>n>>k>>t;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1,0);
	cout<<f[1][o];
	return 0;
}

时空复杂度都是\(O(nk)\)

虽然不如贪心,但我们更重视树形dp的思想。

最关键的是,dp做法经过修改,可以应对有点权的情况,而贪心不行:

点击查看代码
#include<bits/stdc++.h>
#define o k
//为防止影响阅读代码,用o来代替k表示偏移量
using namespace std;
int n,k,t,f[100010][50];
int minn[30],v[100010];//v表示点权
vector<int> G[100010];
void dfs(int pos,int fa){
	bool isleaf=1;
	for(int i:G[pos]) if(i!=fa) isleaf=0,dfs(i,pos);
	if(isleaf){
		for(int i=-k;i<=0;i++) f[pos][i+o]=v[pos];
		//初始化部分,初始值由1改为v[pos]
	}else{
		memset(minn,127,sizeof minn);
		f[pos][-k+o]=v[pos];
		//只有-k状态选自己,所以初始值由1改为v[pos]
		for(int i:G[pos]){
			if(i==fa) continue;
			f[pos][-k+o]+=f[i][k+o];
			for(int t=-k+1;t<=0;t++)
				f[pos][t+o]+=f[i][-t+o],
				minn[t+o]=min(minn[t+o],f[i][t-1+o]-f[i][-t+o]);
			for(int t=1;t<=k;t++)
				f[pos][t+o]+=f[i][t-1+o];
		}
		for(int t=-k+1;t<=0;t++) f[pos][t+o]+=minn[t+o];
		for(int t=-k+1;t<=k;t++)
			f[pos][t+o]=min(f[pos][t+o],f[pos][t-1+o]);
	}
}
int main(){
	cin>>n>>k>>t;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	for(int i=1;i<=n;i++) cin>>v[i];//输入点权
	dfs(1,0);
	cout<<f[1][o];
	return 0;
}

JLOI2016/SHOI2016 侦察守卫

P3267 [JLOI2016/SHOI2016] 侦察守卫

给定一个\(n\)个节点的树形结构。你可以任意选择节点安装摄像头,在当前点安装摄像头的代价就是该点的点权。每个摄像头可以监视与当前节点距离\(\le k\)的节点。接下来给定\(m\)个需要监视的节点,请问如果要把它们全部监视,最少付出的代价是多少?

我们受上面解法的启发,我们仍然定义\(f[pos][t]\)为根节点为\(pos\),状态为\(t\)的答案。

  • \(-k\le t<0\):让\(pos\)及往下所有需要被监视的节点都被覆盖,而至少向上\(1\)层到向上\(-t\)层之间所有节点都被覆盖(注意理解一下,因为我们事先不知道父节点的状态,所以只能这样设状态)。
  • \(t=0\)至少\(pos\)及以下需要监视的节点都被覆盖。
  • \(0<t\le k\)至少\(pos\)向下\(t\)层以下需要监视的节点都被覆盖。

上面我们已经写出了应对有点权情况的代码。接下来考虑如何解决“只保证监视给定的\(m\)个节点”。

首先,我们用\(vis[pos]\)表示该节点是否需要覆盖,需要为\(true\),不需要为\(false\)

首先对于叶子结点\(i\),上一题的初始化是:

  • 对于\(-k\le t\le 0\)\(f[i][t]=1\);对于\(0<t\le k\)\(f[i][t]=0\)

我们需要改成:

  • 如果\(vis[pos]=true\),则对于\(-k\le t\le 0\)\(f[i][t]=v[i]\);对于\(0<t\le k\)\(f[i][t]=0\)
  • 否则,对于\(-k\le t<0\)\(f[i][t]=v[pos]\);对于\(0\le t\le k\)\(f[i][t]=0\)

解释:

  • 首先因为有权值了,所以需要把\(1\)改成\(v[i]\)
  • 然后,如果叶子结点\(i\)自己不必被覆盖,那么显然\(f[i][0]=0\),而根据定义,\(f[i][1\sim k]\)必须全部被覆盖,因此初始值为\(v[i]\)

我们再以上一道题为基础,思考状态转移。

  • 对于\(t=-k\)\(f[pos][t]=\sum\limits_{i是pos子节点}f[i][k]+1\)
    \(1\)改为\(v[pos]\)即可。
  • 对于\(-k<t\le 0\)\(f[pos][t]=\min(\quad\min\limits_{i是pos子节点}(f[i][t-1]+\sum\limits_{j是pos子节点,j\ne i}f[j][-t])\quad,f[pos][t-1])\)
    上面的\(k\)枚举到\(-1\)而非\(0\)
    对于状态\(0\),我们单独考虑:
    • 如果\(vis[pos]=true\),则和上面的转移相同(相当于放到循环里了)。
    • 否则,
      \(\begin{aligned} vis[pos]&=\min(\quad\min\limits_{i是pos子节点}(f[i][t]+\sum\limits_{j是pos子节点,j\ne i}f[j][-t])\quad,f[pos][t-1])\\ &=\min(\sum\limits_{i是pos子节点}f[i][0],f[pos][t-1]) \end{aligned}\)
      解释:因为\(pos\)不需要被覆盖,所以原本覆盖\(-t+1\)层的那个特殊的子节点\(i\),现在只需要覆盖\(-t\)层。
  • 对于\(0<t\le k\)\(f[pos][t]=\min(\sum\limits_{i是pos子节点}f[i][t-1],f[pos][t-1])\)
    不用改。

就酱。

时空复杂度都是\(O(nk)\)

代码仍然通过加一个偏移量\(k\)(即覆盖半径)来处理负数下标的问题。

注意需要双向存图,以及\(f\)的第二维开\(2*k\)的空间。

点击查看代码
#include<bits/stdc++.h>
#define o k
using namespace std;
int n,m,k,f[500010][50];
int minn[30],v[500010];
bool vis[500010];//表示该节点是否强制覆盖
vector<int> G[500010];
void dfs(int pos,int fa){
	bool isleaf=1;
	for(int i:G[pos]) if(i!=fa) isleaf=0,dfs(i,pos);
	if(isleaf){
		for(int i=-k;i<0;i++) f[pos][i+o]=v[pos];
		if(vis[pos]) f[pos][o]=v[pos];
	}else{
		memset(minn,127,sizeof minn);
		f[pos][-k+o]=v[pos];
		int lim=vis[pos]?0:-1;
		for(int i:G[pos]){
			if(i==fa) continue;
			f[pos][-k+o]+=f[i][k+o];
			for(int t=-k+1;t<=lim;t++)
				f[pos][t+o]+=f[i][-t+o],
				minn[t+o]=min(minn[t+o],f[i][t-1+o]-f[i][-t+o]);
			if(lim==-1)
				f[pos][o]+=f[i][o];
			for(int t=1;t<=k;t++)
				f[pos][t+o]+=f[i][t-1+o];
		}
		for(int t=-k+1;t<=lim;t++) f[pos][t+o]+=minn[t+o];
		for(int t=-k+1;t<=k;t++)
			f[pos][t+o]=min(f[pos][t+o],f[pos][t-1+o]);
	}
}
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>v[i];
	cin>>m;
	for(int i=1;i<=m;i++){
		int u;
		cin>>u;
		vis[u]=1;
	}
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1,0);
	cout<<f[1][o];
	return 0;
}

另一种思路

这是题解的思路,设计的确十分精妙易懂。

状态表示方面,和上面设的\(f\)数组本质上是相同的。只不过把负数下标存在\(f\)中,正数下标存在\(g\)中。\(f[pos][0]\)的含义与\(g[pos][0]\)重合。

即:

  • \(f[pos][t]\)\(pos\)及以下需要监视的节点被覆盖,而至少\(pos\)以上\(1\)层到向上\(t\)层之间全部节点都被覆盖的答案。
  • \(g[pos][t]\)\(pos\)往下\(t\)层以下的需要监视的节点被覆盖的答案。

先说最重要的部分——转移,再说初始化。

我们的第一个思路是针对所有子节点一次性计算出来(所以非叶子结点是不需要有任何初始化的)。

而这个做法和树上背包有着异曲同工之妙。即父节点\(pos\)先看作子节点为空,有一个初始状态。接下来不断往\(pos\)添加子树。每添加一个子树就更新一遍\(pos\)的状态。

转移方法:
对于节点\(pos\),遍历其子节点\(i\)
对于每个以\(i\)为根的子树,合并过程如下:

  • 对于\(0\le t\le k\)\(f[pos][j]=\min(f[pos][t]+g[i][t],f[i][t+1]+g[pos][t+1])\)
    解释:
    • \(1\)种情况:之前合并过的部分先出马,向上覆盖了\(t\)个节点,那么新来的子树就只需要用状态\(g[i][t]\),即只关心自己向下第\(t\)层以下被覆盖即可。
    • \(2\)种情况:新来的子树先出马,向上覆盖了\(t+1\)个节点(若想让\(pos\)向上覆盖\(t\)个,子节点需要向上覆盖\(t+1\)个),那么之前合并过的部分就只需要关心自己向下第\(t+1\)层以下被覆盖即可。
  • \(g[pos][0]=f[pos][0]\)。而后对于\(0<t\le k\)\(g[pos][t]=\min(g[pos][t],g[pos][t-1])\)
    解释:首先因为\(g[pos][0]\)\(f[pos][0]\)的含义重合,所以需要单独赋值一下下标\(0\)。然后\(g\)的转移和上一种思路中,正数下标的转移是一样的。根节点的状态\(t\),就相当于所有子节点的状态都是\(t-1\)

注意:和上一种思路相同,\(f\)需要求一个后缀最小值,而\(g\)需要求一个前缀最小值。就相当于上一种思路的\(f[pos][t]=\min(f[pos][t],f[pos][t-1])\)。具体原因见上。

最后说一下初始值:
因为我们是实时合并子树,所以所有节点\(pos\)初始都应当有一个状态。与上面极为相似,可以结合状态定义来理解,就不解释了:

  • 初始\(f,g\)全为\(0\)
  • 对于\(1\le t\le k\)\(f[pos][t]=v[pos]\)
  • 如果\(vis[pos]=true\),则\(f[pos][0]=g[pos][0]=v[pos]\)

时空复杂度同样都是\(O(nk)\),代码十分简洁。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
bool vis[500010];
int n,m,k,v[500010];
int f[500010][30],g[500010][30];
vector<int> G[500010];
void dfs(int pos,int fa){
	for(int t=1;t<=k;t++) f[pos][t]=v[pos];
	if(vis[pos]) g[pos][0]=f[pos][0]=v[pos];
	f[pos][k+1]=INT_MAX;
	for(int i:G[pos]){
		if(i==fa) continue;
		dfs(i,pos);
		for(int t=0;t<=k;t++) f[pos][t]=min(f[pos][t]+g[i][t],f[i][t+1]+g[pos][t+1]);
		for(int t=k;t>=0;t--) f[pos][t]=min(f[pos][t],f[pos][t+1]);
		g[pos][0]=f[pos][0];
		for(int t=1;t<=k;t++) g[pos][t]+=g[i][t-1];
		for(int t=1;t<=k;t++) g[pos][t]=min(g[pos][t],g[pos][t-1]);
	}
}
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>v[i];
	cin>>m;
	for(int i=1;i<=m;i++){
		int u;
		cin>>u;
		vis[u]=1;
	}
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1,0);
	cout<<g[1][0];
	return 0;
}

[To Be Continued]
(树的直径相关,会单独开一个新笔记)

posted @ 2024-05-04 10:59  Sinktank  阅读(36)  评论(0编辑  收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2024 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.