[笔记]树形dp - 1/4(节点选择类)
树形dp,是一种建立在树形结构上的dp,因此dfs一般是实现它的通用手段。
是一种很美的动态规划呢。
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
在一棵树中,找到若干个点,每个点放置\(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 最大子树和
在一棵树中选择一块联通分量,让其点权和最大。
用\(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 消防局的设立
此题和上道题很像,只不过建立一个消防站可以扑灭距离在\(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 将军令
在一个树形结构中选若干个点驻扎军队,每个军队可以看守到所在节点距离不超过\(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]
(树的直径相关,会单独开一个新笔记)