树形dp小总结(换根,基环树,杂七杂八的dp)

脑子是个好东西,可惜我有的不多。。。从小到大都是看见树就头皮发麻,前阵子学的多了稍微好一点,但是经过一个树专题的洗礼,熟悉的恐惧感又回来了。。。

会讲一下换根dp,基环树dp,也许后面还会更一下树上背包 之类的 

目录

换根dp

例1

例2 

 例3

中场练习 

例4

例5(鸽 

练习:

基环树dp

祖传老题

例2

例3

杂题

例1

 例2

例3

例4 

例5 


换根dp

换根换根,就是对不同节点做根的情况进行讨论,通常就是在一些节点选择的题目里会出现,但是碰到的不多

(lwh:简单的不会考,难的,你们。。。

例1

换根dp-入门

大意:

选择一个节点,使得以它作树的根时,整棵树的节点深度之和最大

思路:
dp都是由子状态转移过来的,这里我们选择的子状态就是任意一个初始情况,然后依次向下更新就行了

当一个节点向它的儿子转移时,

从A节点向B节点转移,我们考察树上每一个节点的深度变化。

A节点由根变成一个子节点,深度++,则图中右侧部分的左右节点深度也会++

B节点由一个儿子变成根,深度--,则图中左侧部分的节点深度也会--

那么状态转移时的深度变化就很明显了

令dp【i】表示以i为根时的节点深度和,siz【i】表示以i为根的子树的节点个数,n表示总节点数,有:

dp【son】=dp【i】-siz【son】+n-siz【son】 

所以我们只要先预处理出一个根节点的dp值,乖乖转移就好了

板子题

code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1e6+10;
const ll mod=1e9+7;
const ll inf=0x3f3f3f3f3f3f3f3f;
struct ty{
	ll l,t,next;
}edge[N<<1];
ll cnt=0;
ll head[N];
ll n,a,b,c;
ll mas[N];
void add(ll a,ll b,ll c)
{
	edge[++cnt].l=c;
	edge[cnt].t=b;
	edge[cnt].next=head[a];
	head[a]=cnt;
}
ll dp[N];
ll siz[N];
void dep_dfs(ll id,ll p,ll d)
{
	dp[1]+=d;
	siz[id]=1;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dep_dfs(y,id,d+1);
		siz[id]+=siz[y];
	}
}
void dfs(ll id,ll p)
{
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dp[y]=dp[id]-siz[y]+(n-siz[y]);
		dfs(y,id);
	}
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	memset(head,-1,sizeof head);
	cin>>n;
	for(int i=1;i<n;++i)
	{
		cin>>a>>b;
		add(a,b,1);
		add(b,a,1);
	}
	dep_dfs(1,-1,1);
//	for(int i=1;i<=n;++i) cout<<siz[i]<<" ";
	dfs(1,-1);
	ll maxn=0;
	ll p=0;
	for(int i=1;i<=n;++i)
	{
	//	cout<<dp[i]<<" ";
		if(maxn<dp[i])
		{
			maxn=dp[i];
			p=i;
		}
	}//maxn=max(maxn,dp[i]);
	cout<<p<<endl;
	return 0;
}

例2 

上点难度

大意:

一棵树,有边权,有点权,选定根节点最小化节点到根节点的加权路径和(sum(点权*路径长))

思路:

跟原本的思路其实差不多,只要按根节点转移,考虑一下中间的变化就好了 

一个点转移到它的儿子时,设它们的边权为w,儿子的子树里的点需要的距离都会减少一个w*点权,而其余的点要增加一个w*点权

就没了~

具体看代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
const ll inf=0x3f3f3f3f3f3f3f3f;
ll t;
struct ty{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll a,b,c,n;
ll mas[N];
ll sum[N];
ll dp[N];//以i为根的总路程 
void init_dfs(ll id,ll p)
{
	sum[id]=mas[id]; 
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		init_dfs(y,id);
		sum[id]+=sum[y];
		dp[id]+=dp[y]+edge[i].l*sum[y];
	}
}

void dfs(ll id,ll p)
{
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dp[y]=dp[id]-sum[y]*edge[i].l+(sum[1]-sum[y])*edge[i].l;
		dfs(y,id);
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>mas[i];
	for(int i=1;i<n;++i)
	{
		cin>>a>>b>>c;
		add(a,b,c);
		add(b,a,c);
	}
	init_dfs(1,-1);
	//for(int i=1;i<=n;++i) cout<<dp[i]<<' ';
	dfs(1,-1);
	ll maxn=inf;
	for(int i=1;i<=n;++i) maxn=min(maxn,dp[i]);
	cout<<maxn;
}
int main()
{
	//cin>>t;while(t--)
	solve();
	return 0;
}

 例3

难度升级

大意:
给你一棵 n 个点的树,点带权,对于每个节点求出距离它不超过 k 的所有节点权值和 mi

思路:
同样时对每一个节点的情况进行考虑,转移

我们不妨设两个数组f,dp

令f[i][j]表示i节点往下j步内能到达的点数之和,dp[i][j]表示i节点往上和往下j步能到达的点数之和

那么更新的方程也就呼之欲出了。老样子,儿子节点我们就叫它son

dp[i][0]=f[i][0](显然)

dp[son][1]=dp[son][0]+f[i][0](每一个儿子的父节点都可以一步到达)

dp[son][j]=f[son][j]+dp[i][j-1]-f[son][j-2](一个小容斥,第一部分就是儿子节点往下能在j步内到达的节点数,我们还应该加上它往上能更新的节点数,加上dp[i][j-1]的话,其实有一部分是算重了,就是f[son][j-2],减掉就好了)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
const ll inf=0x3f3f3f3f3f3f3f3f;
ll t;
struct ty{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll a,b,c,n,k;
ll mas[N];
ll f[N][25];
ll dp[N][25];
void init_dfs(ll id,ll p)
{
	//f[id][0]=mas[id];
	for(int i=0;i<=k;++i)f[id][i]=mas[id];
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		init_dfs(y,id);
		for(int j=1;j<=k;++j) f[id][j]+=f[y][j-1];	
	}
}

void dfs(ll id,ll p)
{
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dp[y][1]+=f[id][0];
		for(int j=2;j<=k;++j)
		dp[y][j]+=dp[id][j-1]-f[y][j-2];
		dfs(y,id);
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	memset(f,0,sizeof f);
	cin>>n>>k;
	for(int i=1;i<n;++i)
	{
		cin>>a>>b;
		add(a,b,1);
		add(b,a,1);
	}
	for(int i=1;i<=n;++i) cin>>mas[i];
	init_dfs(1,-1);

	for(int i=1;i<=n;++i)
	{
		for(int j=0;j<=k;++j) 
		{
			//cout<<f[i][j]<<" ";
			dp[i][j]=f[i][j];
		}
	//	cout<<endl;
	}
	
	dfs(1,-1);
	for(int i=1;i<=n;++i) cout<<dp[i][k]<<endl;
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	//cin>>t;while(t--)
	solve();
	return 0;
}

中场练习 

CF1187E Tree Painting​​​​​​ (看完前三道,这道应该很快就穿了~)

再来一道狠的

例4

CF708C Centroids

一道CF2300的题,有点绕

大意:

给定一颗树,你有一次将树改造的机会,改造的意思是删去一条边,再加入一条边,保证改造后还是一棵树。

请问有多少点可以通过改造,成为这颗树的重心?(如果以某个点为根,每个子树的大小都不大于n/2,则称某个点为重心)

思路:
关于这个操作,其实就是要将一个较大子树(大小超过n/2)的一部分分出去,使得所有子树的最大值不超过n/2,这是一个贪心的思想

另外我们注意到一个很有意思的性质,如果一个点已经是重心了的话,那么它往下的儿子的子树肯定也是小于n/2的,换句话说,如果我们从一个重心开始往下转移,一个节点是否能够成为重心,就在于它的父节点对应的子树在取出尽可能大的一部分后是否能够坐到大小<=n/2,因为该节点对应的子树大小肯定是小于n/2的。

 为此,在找到重心的前提下,我们需要维护对于每一个节点的子树中大小<=n/2的最大子树的大小,叫它ma

对于一个节点u,只要n-siz[u]-ma<=n/2,这个节点就可以作为重心了

那么好像只要在dfs时顺便用maxn处理一下就可以了?

当然不行(我都这么问了

对于一个节点u来说,可能它父亲的最大合法子树就是u所对应的子树,但是我们想删的是除了u的子树以外的最大子树(看图理解), 

所以我们得维护一个最大值和一个次大值,如果最大值跟u的子树大小相等,取次大值就好了

可以用一个maxn[i][j]来维护,j取0/1代表最大值/次大值

接下来考虑如何维护maxn数组:由子树大小更新即可(前提是子树大小不超过n/2)

思路:

1.找到重心

2.从重心开始,维护每一个节点的子树大小&maxn数组

3.从重心开始,进行转移,对于节点u,取ma=其父节点的最大/次大子树大小(取哪个看上文),按照上文提到的判断式进行判断即可

细节见code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=4e5+10;
const ll inf=0x3f3f3f3f3f3f3f3f;
ll t;
struct ty{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll a,b,c,n,k;
ll rt;//重心 
ll siz[N];//子树大小 
ll maxn[N][3];//i的最大子树/次大子树 
bool vis[N];
void root_dfs(ll id,ll p)//找到重心
{
	bool fl=1;
	siz[id]=1;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		root_dfs(y,id);
		siz[id]+=siz[y];
		if(siz[y]>n/2) fl=0;
	}
	if(n-siz[id]>n/2) fl=0;
	if(fl) rt=id;
}
maxn_dfs(ll id,ll p)//更新maxn数组,注意这里是以新的重心作为根了,所以siz数组要重新做!!!
{
	siz[id]=1;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		maxn_dfs(y,id);
		siz[id]+=siz[y];
		if(siz[y]>n/2) continue;
		else if(siz[y]>maxn[id][0]) maxn[id][1]=maxn[id][0],maxn[id][0]=siz[y];
		else if(siz[y]>maxn[id][1]) maxn[id][1]=siz[y];
	}
}
void dfs(ll id,ll p,ll ma)
{//如果n-siz[id]-cnt<n/2 id可以作为重心 
    //cout<<id<<" "<<p<<' '<<ma<<" "<<siz[id]<<endl;
    ll cnt=ma;
    if((id==rt)||(n-siz[id]-cnt<=n/2)) vis[id]=1;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		if(n-siz[id]<=n/2) ma=max(ma,n-siz[id]);//可能它的父节点可以直接成为一个合法子树
		if(siz[y]==maxn[id][0]) dfs(y,id,max(maxn[id][1],ma));
		else dfs(y,id,max(maxn[id][0],ma)); 
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	cin>>n;
	for(int i=1;i<n;++i)
	{
		cin>>a>>b;
		add(a,b,1);
		add(b,a,1);
	}
	root_dfs(1,-1);
	memset(siz,0,sizeof siz);//siz数组重新做
	maxn_dfs(rt,-1);
	dfs(rt,-1,0);
	for(int i=1;i<=n;++i) cout<<vis[i]<<' ';
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	//cin>>t;while(t--)
	solve();
	return 0;
}

例5(鸽 

[COCI2014-2015#1] Kamp

挺烦的一道题,还没做出来,先鸽着(不小心就鸽到退役

练习:

Equidistant(也可以bfs做,很骚)

简单树上操作

AT4543 Subtree

Promises I Can't Keep

Kamp

---------------------------------------------------------------------

基环树dp

基环树,就是一个树上又多了一条边,那么就有了一个环,树上一个环=基环树。处理这种问题,一般是将环断开,再对两个断开的端点分别处理。

祖传老题

城市环路

大意:

给定一个无向基环树,带点权,边上两点只能选其一,求最大点权和

思路:
先不看环,这不就是经典永流传的没有上司的舞会嘛。现在加了一条边,有了一个环,自然不能像原来那样处理,因为原本的是一个树形结构。

但是,如果把环断开(也就是把多余的边删了),那不就是舞会了吗

换句话说,我们可以做两遍舞会,然后取最大值就好啦

细节见code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e5+10;
const ll inf=0x3f3f3f3f3f3f3f3f;
ll t;
struct ty{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll a,b,c,n;
double mas[N];
ll fa[N];
ll U,V;
double k;
double dp[N][3];
ll find(ll x)
{
	return fa[x]==x?x:fa[x]=find(fa[x]);
}
void dfs(ll id,ll p)//经典舞会操作
{
	dp[id][0]=0.000,dp[id][1]=mas[id]*k*1.000;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dfs(y,id);
		dp[id][0]+=max(dp[y][0],dp[y][1]);
		dp[id][1]+=dp[y][0]*1.000;
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	cin>>n;
	for(int i=0;i<=n+1;++i) fa[i]=i;
	for(int i=1;i<=n;++i) cin>>mas[i];
	for(int i=1;i<=n;++i)
	{
		cin>>a>>b;
		a++;b++;
		ll fx=find(a);ll fy=find(b);
		if(fx==fy)//并查集判环,如果两者在一个集合里,就不连这条边,相当于将边断开
		{
			U=a;V=b;
			continue;
		}
		add(a,b,1);
		add(b,a,1);
		fa[fx]=fy;//merge 
	}
	cin>>k;
	//cout<<U<<" "<<V<<endl;
	dfs(U,-1);
	double ans=dp[U][0];
	//cout<<dp[U][0]<<' '<<dp[U][1]<<endl;
	dfs(V,-1);
	ans=max(ans,dp[V][0]);
	cout<<fixed<<setprecision(1)<<ans*1.00<<endl;
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	//cin>>t;while(t--)
	solve();
	return 0;
}

例2

骑士

大意:
有一群骑士,每一个人有一个仇视的人,他们两个人不能在一起

现在要选出一些人,不互相矛盾且战斗力之和最大

思路:
首先,n个人n条边,这是一颗基环树

仔细分析不难发现,这个跟没有上司的舞会其实有异曲同工之处。如果把每一个人仇视的人作为它的父亲节点的话,每一个点的入度都是1,我们就可以通过预处理一个fa数组表示每一个节点的父亲,来通过一个节点找到环。找到环之后,跟原来的处理方法一样,断开环,对两个端点做一次舞会,并且强制规定端点不选,就能找到最大值了

另外,这张图有可能是一个基环树森林,所以我们要不断进行找环-断环-dp的过程,每一个过程让ans累加对应的值,这样就可以了

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=1e6+10;
struct ty{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll fa[N];//父亲节点 
ll n,a,b;
ll mas[N];
bool vis[N];
ll ans=0;
ll dp[N][3];//经典舞会
ll root;
void dfs(ll id)
{
	vis[id]=1;//访问过了 
	dp[id][0]=0,dp[id][1]=mas[id];
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==root) continue;
		dfs(y);
		dp[id][0]+=max(dp[y][0],dp[y][1]);
		dp[id][1]+=dp[y][0];
	}
 } 
void solve(ll id)
{
	root=id;
	vis[id]=1;//更新
	while(!vis[fa[root]])
	{
	    root=fa[root];
		vis[root]=1;	
	} 
	dfs(root);
	ll t=dp[root][0];
	vis[root]=1;
	root=fa[root];
	dfs(root);
	ans+=max(t,dp[root][0]);
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n;
	memset(head,-1,sizeof head);
	for(int i=1;i<=n;++i)
	{
		cin>>mas[i];//战斗力 
	    cin>>a;
		add(a,i,1);
		fa[i]=a;//最讨厌的人是他的父亲节点 
	} 
	for(int i=1;i<=n;++i)
	{
		if(!vis[i]) solve(i);
	}
	cout<<ans;
	return 0;
}

例3

旅行

大意:
给定n个点,m条边,m=n-或者m=n

从任意一个点出发,要求将所有点都经过经过一遍且遍历序号的字典序最小

遍历规则:

 思路:

m=n-1时是很好解决的,只要最每个点的出点排个序dfs即可

m=n时,这是一颗基环树。遍历n个点的话,很明显我们只会经过n-1条边,所以有一条边是多余的

我们只要枚举这一条边,删掉它再跑一遍dfs,最后n遍下来取最小字典序方案即可

就是这么暴力。。。(要开O2)

还有看到一种nlogn的做法,有空再补

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define endl '\n'
const ll N=5010;
vector<ll> vt[N];
ll n,m;
ll a,b;
ll edge[N][3];
bool vis[N];//sb[i]:边是否有效 
ll ale,sop;
ll ans[N];
ll res[N];
ll cnt=0;
inline int read()
{
	int X=0; bool flag=1; char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
	while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+ch-'0'; ch=getchar();}
	if(flag) return X;
	return ~(X-1);
}

inline void dfs(ll id)
{
	printf("%d ",id);
	vis[id]=1;
	for(int i:vt[id])
	{
		if(!vis[i]) dfs(i); 
	}
}
inline void dfss(ll id,ll p)
{
	res[++cnt]=id;vis[id]=1;
	for(int i:vt[id])
	{
		if(i==p) continue;
		if(((i==ale&&id==sop)||(i==sop&&id==ale))) continue;//如果是两个断点就不能走
		if(!vis[i]) dfss(i,id);
	}
}
inline bool cmp()//看ans数组是否可以更新
{
	for(int i=1;i<=n;++i)
	{
		if(ans[i]!=res[i]) return ans[i]>res[i];
	}
	return 0;
}
int main()
{//ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	n=read();m=read();
	for(int i=1;i<=m;++i)
	{
		a=read();b=read();
		vt[a].push_back(b);
		vt[b].push_back(a);
		edge[i][0]=a;edge[i][1]=b;
	}
	for(int i=1;i<=n;++i) sort(vt[i].begin(),vt[i].end());
	if(m!=n)
	{
		dfs(1);
		return 0;
	}
	
	for(int i=1;i<=n;++i) ans[i]=1e8;
	for(int i=1;i<=m;++i)
	{
		cnt=0;
		for(int j=1;j<=n;++j)
		{
			vis[j]=0;
			res[j]=0;
		}
		sop=edge[i][0];ale=edge[i][1];//枚举的两个断点,它们之间的边不能走
		dfss(1,-1); 
		if(cmp()&&cnt==n) memcpy(ans,res,sizeof(res));
	}
	for(int i=1;i<=n;++i) printf("%d ",ans[i]);
	return 0;
}

------------------------------------------------------------------

杂题

一般是背包/各种奇奇怪怪的dp

例1

ABC F - Select Edges

大意:
给定一棵树,现在要保留其中的一部分边,每一个节点最多只能与di条边相连,求留下来的边的最大权值和

思路:
考虑dp[i][j]表示节点i连j条边的子树最大权值和,但是数据范围开不下

所以考虑优化,第二维缩成2,dp[i][0]表示最多连di-1条边的子树最大权值,dp[i][1]表示最多连di条边的子树最大权值和,也就是最优解

那么考虑转移,每一个节点有两种方式从子节点转移过来

1.不连边,那么就是直接加上dp[son][1]

2.连边,那么就是加上dp[son][0]+w(边权)

那我们要怎么选呢?

其实可以一开始全部加上选项1,然后将选项2-选项1的结果存起来,最后排个序再把对应的贡献加回去,就好了。

还有如果di=0的话,注意把dp[i][0]处理成-inf,因为这种情况现在是非法的

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3e5+10;
const ll mod=1e9+7;
const ll inf=0x3f3f3f3f3f3f3f3f;
struct ty{
	ll l,t,next;
}edge[N<<1];
ll cnt=0;
ll head[N];
ll n,a,b,c;
ll mas[N];
void add(ll a,ll b,ll c)
{
	edge[++cnt].l=c;
	edge[cnt].t=b;
	edge[cnt].next=head[a];
	head[a]=cnt;
}
ll dp[N][3];
void dfs(ll id,ll p)
{
	vector<ll> vt;
	
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dfs(y,id);
		dp[id][1]+=dp[y][1];
		dp[id][0]+=dp[y][1];
		vt.push_back(dp[y][0]+edge[i].l-dp[y][1]);
	}
	ll cnt=0;
	sort(vt.begin(),vt.end(),greater<ll>());
	for(int i=0;i<vt.size();++i)
	{
		cnt++;
		if(vt[i]<0) break;
		if(cnt<=mas[id]-1) dp[id][0]+=vt[i];
		if(cnt<=mas[id]) dp[id][1]+=vt[i];
		if(cnt>mas[id]) break;
	}
	if(mas[id]==0) dp[id][0]=-inf;
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	memset(head,-1,sizeof head);
	cin>>n;
	for(int i=1;i<=n;++i) cin>>mas[i];
	for(int i=1;i<n;++i)
	{
		cin>>a>>b>>c;
		add(a,b,c);
		add(b,a,c);
	}
	dfs(1,-1);
	cout<<dp[1][1]<<endl;
	return 0;
}

 例2

树上染色

大意:

思路:
考虑每一条边对答案的贡献, 很明显就是它被经过的次数。次数怎么算,就是它两边同色的节点个数之积(显然

也就是  贡献次数=k*(m-k)+(siz[y]-k)*(n-m-siz[y]+k),k为当前子树下选掉的点 ,y就是边的一个顶点,第一部分就是黑色的贡献,第二部分就是白色的贡献

那么取一个dp[i][j]代表以i为根的子树取j个黑点的贡献

显然 dp[id][0]=dp[id][1]=0;

那么我们只要倒序枚举第二位进行更新就好了

还有一个要注意的点,就是子树全白也是对答案有贡献的,这个要另外讨论

细节见code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2010;
const ll mod=1e9+7;
const ll inf=0x3f3f3f3f3f3f3f3f;
struct ty{
	ll l,t,next;
}edge[N<<1];
ll cnt=0;
ll head[N];
ll n,a,b,c,m;
ll mas[N];
void add(ll a,ll b,ll c)
{
	edge[++cnt].l=c;
	edge[cnt].t=b;
	edge[cnt].next=head[a];
	head[a]=cnt;
}
ll dp[N][N];//以i为根的子树中选择j个点的对答案的最大贡献
ll siz[N];//子树大小 
void dfs(ll id,ll p)
{//贡献次数=k*(m-k)+(siz[y]-k)*(n-m-siz[y]+k),k为当前子树下选掉的点 
 //总贡献=贡献次数*边权   
	siz[id]=1;
	dp[id][0]=dp[id][1]=0;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
	    ll y=edge[i].t;
		if(y==p) continue;
		dfs(y,id);
		siz[id]+=siz[y];
		for(int j=min(m,siz[id]);j>=0;--j)
		{
			if(dp[id][j]!=-1)//合法
			{//子树全白的情况 
			    dp[id][j]+=dp[y][0]+siz[y]*(n-m-siz[y])*edge[i].l;
			} 
			for(int k=min((ll)j,siz[y]);k;--k)
			{
				if(dp[id][j-k]==-1) continue;//该状态非法,其它子树的可选的黑点数目还不到j-k,因此无法更新
				ll val=k*(m-k)+(siz[y]-k)*(n-m-siz[y]+k);
				dp[id][j]=max(dp[id][j],dp[id][j-k]+dp[y][k]+val*edge[i].l);
			}
		}
	}	
} 
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	memset(head,-1,sizeof head);
	cin>>n>>m;
	for(int i=1;i<n;++i)
	{
		cin>>a>>b>>c;
		add(a,b,c);
		add(b,a,c);
	}
	memset(dp,-1,sizeof dp);
	memset(siz,0,sizeof siz);
	dfs(1,-1);
	cout<<dp[1][m];
	return 0;
}

例3

hdu Tree

大意:
给一个树涂上k种颜色,对于每一种颜色,都可以唯一确定一张所有点同色的DAG,求所有DAG的边的交集的最大边数

思路:
找方案是不可能的,这辈子都不会去找方案。跟上一题一样,考虑每一条边的贡献

显然,贡献要么是1,要么是0.

什么时候会是1

就是这条边的左右两边,每种颜色的点都有(在树上两点的最短路径是唯一的,所以满足该情况时这条边一定会被经过)

换句话说,考察这条边的一个端点u

如果siz[u]>=m&&(n-siz[u])>=m ans++;

就没了~个人觉得真的是非常巧妙

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+10;
const ll mod=1e9+7;
const ll inf=0x3f3f3f3f3f3f3f3f;
struct ty{
	ll l,t,next;
}edge[N<<1];
ll cnt=0;
ll head[N];
ll n,a,b,c,m,t;
ll mas[N];
void add(ll a,ll b,ll c)
{
	edge[++cnt].l=c;
	edge[cnt].t=b;
	edge[cnt].next=head[a];
	head[a]=cnt;
}
ll siz[N];//子树大小 
ll ans=0;
void dfs(ll id,ll p)
{
	siz[id]=1;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dfs(y,id);
		siz[id]+=siz[y];
		if(siz[y]>=m&&(n-siz[y])>=m) ans++;
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	memset(siz,0,sizeof siz);
	ans=0;cnt=0;
	cin>>n>>m;
	for(int i=1;i<n;++i)
	{
		cin>>a>>b;
		add(a,b,1);
		add(b,a,1);
	}
	dfs(1,-1);
	cout<<ans<<endl;
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>t;while(t--)
	{
		solve();
	}
	
	return 0;
}

例4 

Equidistant

是作为练习放在换根dp里面了,这里提供一个bfs的做法

大意:
一棵树里选m个特殊点,问是否有一点满足它到所有的特殊点的距离都相等,若有,输出任意一个

思路:

考虑dis【】来表示每一个点到所有特殊点的最短路,以及cnt【】表示有几个特殊点到该点的最短路=dis

显然,如果有一个点的cnt=m,他就是答案

更新的时候,如果下一个点的dis=当前dis+1,说明两个点的对所有特殊点的地位是一样的,那么下一个点的cnt就可以直接继承该点的cnt

还有一种情况见代码,懒得码字了。。。

(不过这个比换根好写很多。。。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2e5+10;
const ll mod=1e9+7;
const ll inf=0x3f3f3f3f3f3f3f3f;
struct ty{
	ll l,t,next;
}edge[N<<1];
ll cn=0;
ll head[N];
ll n,a,b,c,m,t;
ll mas[N];
void add(ll a,ll b,ll c)
{
	edge[++cn].l=c;
	edge[cn].t=b;
	edge[cn].next=head[a];
	head[a]=cn;
}
ll siz[N];//子树大小 
queue<ll> q;
ll cnt[N],dis[N];
void f()
{
	while(!q.empty())
	{
		ll ty=q.front();
		q.pop();
		for(int i=head[ty];i!=-1;i=edge[i].next)
		{
			ll y=edge[i].t;
			if(dis[y]==dis[ty]+1) cnt[y]+=cnt[ty];
			else if(dis[y]>dis[ty]+1)
			{
				dis[y]=dis[ty]+1;
				cnt[y]=cnt[ty];
				q.push(y);
			}
		}
	}
}
void solve()
{
	memset(head,-1,sizeof head);
	memset(dis,0x3f,sizeof dis);
	cin>>n>>m;
	for(int i=1;i<n;++i)
	{
		cin>>a>>c;
		add(a,c,1);
		add(c,a,1);
	}
	for(int i=1;i<=m;++i) cin>>mas[i],cnt[mas[i]]=1,dis[mas[i]]=0,q.push(mas[i]);
	f();
	for(int i=1;i<=n;++i)
	{
		if(cnt[i]==m)
		{
			cout<<"YES"<<endl<<i;
			return;
		}
	}
	cout<<"NO"<<endl;
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	//cin>>t;while(t--)
	solve();
	
	return 0;
}

例5 

时态同步

大意:

给定一棵树(根给定),可以在一些边上操作使其权值++,求最小的操作数满足根到所有叶节点的权值和相同

思路:

虽然在洛谷上是一道蓝题,但是水的一。。。

首先,最大权值的那条链肯定不能动,而且很容易发现,权值+1的操作最好在深度较低的地方进行,因为这样可以对尽可能多的点造成影响

既然如此,一遍dfs找到最大权值和,并记录每一个点作为根时的子树权值最大值和最小值

第二次dfs时,如果子树权值最小值都满足要求了,整棵子树就满足要求了,否则给对应的边加上最大全值与目标的差值并继续dfs即可

细节见code

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define endl '\n'
const ll N=1e6+10;
struct ty{
	ll t,l,next;
}edge[N<<1];
ll cn=0;
ll head[N];
void add(ll a,ll b,ll c)
{
	edge[++cn].t=b;
	edge[cn].l=c;
	edge[cn].next=head[a];
	head[a]=cn;
 } 
ll n;
ll s;
ll a,b,c;
ll cnt=0;
ll dp[N];//以i为子树根起点的最大路径长 
ll ddp[N];//以i为子树根起点的最短路径长 
ll du[N];
void dfs(ll id,ll p)
{
	if(id!=s&&du[id]==1) dp[id]=ddp[id]=0;
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		dfs(y,id);
		dp[id]=max(dp[id],dp[y]+edge[i].l); 
		ddp[id]=min(ddp[id],ddp[y]+edge[i].l); 
	}
}
void exdfs(ll id,ll p,ll add,ll tar)//tar:目标长度 
{
	for(int i=head[id];i!=-1;i=edge[i].next)
	{
		ll y=edge[i].t;
		if(y==p) continue;
		//cout<<tar<<" "<<ddp[y]+add+edge[i].l<<endl;
		if(tar==ddp[y]+add+edge[i].l) continue;
		ll det=tar-dp[y]-add-edge[i].l;//最多能加的差值
		cnt+=det;
		exdfs(y,id,add+det,tar-edge[i].l); 
	}
	
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n>>s;
	memset(head,-1,sizeof head);
	memset(ddp,0x3f,sizeof ddp);
	for(int i=1;i<n;++i)
	{
		cin>>a>>b>>c;
		add(a,b,c);
		add(b,a,c);
		du[a]++;du[b]++;
	}
	dfs(s,-1); 
	exdfs(s,-1,0,dp[s]);
	cout<<cnt<<endl;
	return 0;
}

ABC263 F - Tournament

大意:

 

思路:

 其实不难想到这是一个树形dp,因为总的操作结果就是由2^n变成1,反过来的话就很像一个树上递归的过程。

所以不妨设dpi,j表示第i个节点赢j次对应的最大总贡献,那么我们就是求dp1,0.也就是从结果的胜者开始往下递归

那么就是考虑节点i的左右子树获胜的最大可能值就好了,边界条件就是底层的那些节点,具体实现看代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const ll N=20;
const ll mod=998244353;
ll n;
ll mp[(1<<17)][N];
ll dp[(1<<17)][N];
ll dfs(ll id,ll k)
{
	if(id>=(1<<n)) return mp[id^(1<<n)][k];
	if(dp[id][k]>0) return dp[id][k];
	ll ans=max(dfs(id<<1,k+1)+dfs(id<<1|1,0),dfs(id<<1,0)+dfs(id<<1|1,k+1));
	return dp[id][k]=ans;
}
int main()
{
	cin>>n;
	memset(dp,-1,sizeof dp);
	for(int i=0;i<(1<<n);++i)
	{
		for(int j=1;j<=n;++j)
		{
			cin>>mp[i][j];
		}
	}
	cout<<dfs(1,0)<<endl;
	return 0;
}

未完待续... 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值