树形规划 换根法

也不知道自己之前颓来颓去的都学了写啥


简析

换根法
主要解决以不同点为根进行动态规划,进而求得最优解的一类题目的解法

这种题目能够从朴素解法中找到突破口,往往是发现根的子节点与根之间的关系,进而能够O(1)换根,使得整个算法的复杂度可以降低一个维度

根节点与子节点的交换是本解法的核心

题目

[USACO10MAR]Great Cow Gathering G

题目
初看此题,很容易想到一种\(O(n^{2})\)的朴素解法
依次枚举根节点再
进行一遍根结点的单源最短路,再枚举每个节点计算贡献

但是这种写法是对正解的引导是很弱的,我们可以换一种方向考虑
\(f[x]\)维护x的子树到\(x\)的贡献,相当于我们已经把\(x\)子树中所有的权值都集中于\(x\)点,考虑再向前一步其中的贡献是

\[edge[i] * size[x] \]

再累积上\(f[x]\)就是\(x\)子树对其父节点的贡献

到了这一步,换根的写法就很容易了

注意

\(INF\) 记得开到\(2^{62}\)
血的教训\(!!!\)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>

using namespace std;

#define INF 1ll<<62
#define int long long 

const int p=1e5+5;

template<typename _T>
inline void read(_T &x)
{
	x=0;char s=getchar();int f=1;
	while(s<'0'||'9'<s){f=1;if(s=='-')f=-1;s=getchar();}
	while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
	x*=f;
}

int head[p],ver[2*p],nxt[2*p],edge[2*p];
int tit,c[p];
int dis[p],inque[p];
int size[p];

void add(int x,int y,int z)
{
	edge[++tit] = z;
	ver[tit] = y;
	nxt[tit] = head[x];
	head[x] = tit;
}

inline void spfa()
{
	memset(dis,0x3f,sizeof(dis));
	dis[1]=0;
	inque[1]=1;
	queue<int> q;
	q.push(1);
	while(q.size())
	{
		int x = q.front();
		q.pop();
		inque[x] = 0;
		for(int i=head[x],v,val;i;i=nxt[i])
		{
			val =edge[i];
			v=ver[i];
			if(dis[v] > dis[x] + val) 
			{
				dis[v] = dis[x] + val;
				if(!inque[v])
				{
					inque[v] =1;
					q.push(v);
				}
			}
		}
	}
}

int tot = 0;
int f[p];
inline void dfs(int now,int fa)
{
	size[now] =c[now];
	
	for(int i=head[now],v ;i ;i=nxt[i])
	{
		v= ver[i];
		
		if(v == fa) continue;
		
		dfs(v,now);
		
		size[now]+=size[v];
	}
}

inline void dfs2(int now,int fa)
{
	for(int i=head[now],v ;i ;i=nxt[i])
	{
		v= ver[i];
		
		if(v == fa) continue;
		
		dfs2(v,now);
		
		f[now] += size[v] * edge[i]+f[v];	
	}
}
int op;
inline void recall(int now,int v,int z)
{
	size[now] = size[now] -size[v];
	op = size[v];
	size[v] = tot;
	f[now] -=f[v] + z*op ;
	f[v] +=size[now]*z + f[now];
	
}

int minn =INF;

inline void bfs(int now,int fa)
{
	minn = min(minn,f[now]);
	
	for(int i=head[now] ,v,val;i; i=nxt[i])
	{
		v=ver[i];
		val = edge[i];
		if(v == fa) continue;
		recall(now,v,val);
		bfs(v,now);
		recall(v,now,val);
		
	}
}

signed main()
{
	int n;
	
	read(n);
	
	for(int i=1;i<=n;i++) 
	{
		read(c[i]);
		tot+=c[i];
	}
	
	for(int i=1,x,y,z;i<n;i++)
	{
		read(x);
		read(y);
		read(z);
		add(x,y,z);
		add(y,x,z);
	}
	
	spfa();
	
	dfs(1,0);
	dfs2(1,0);
	bfs(1,0);
	
	cout<<minn;
}

P1122 最大子树和

题目
依照题目进行动态规划,设计出如下方程
\(f\)数组是保留\(u\)的情况下最大指数和

\[f[u]= \sum_{v∈son(u) 且f[v] > 0} {f[v]} \]

由此方程得,最终求得的最优解必然受到\(u\)得影响,所以只要枚举每个点为根进行一遍动态规划取最值,即是答案

暴力枚举复杂度为\(O(n^{2})\),考虑换根法
在换根时,我们依然借助上式思路,换根时已求的\(f[v]\)最优值
所以只需累计\(f[u]\)的值即可,但\(f[u]\)为负则会起负影响,所以根据\(f[u]\)符号,考虑是否纳入\(f[v]\)

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

#define INF 1<<30
#define int long long 

const int p=16000;

template<typename _T>
inline void read(_T &x)
{
	x=0;char s=getchar();int f=1;
	while(s<'0'||'9'<s){f=1;if(s=='-')f=-1;s=getchar();}
	while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
	x*=f;
}

int point[p];
int head[p],ver[2*p],nxt[2*p];
int size[p];
int f[p];
int tit;
int maxn = 0;

inline void add(int x,int y)
{
	ver[++tit] = y;
	nxt[tit]  =head[x];
	head[x] =tit;
}

inline void dfs(int now,int fa)
{
	int cnt= 0;
	size[now] = point[now];
	f[now] = point[now];
	
	for(int v,i=head[now] ;i;i=nxt[i])
	{
		v=ver[i];
		if(v ==fa) continue;
		dfs(v,now);
		size[now]+=size[v];
		if(f[v]>0)f[now]+=f[v];
		cnt++;
	}
	
	if(cnt==0){
	f[now] = max(0ll,point[now]);
	}
}

inline void recall(int now,int v)
{
	if(f[v] > 0) 
	f[now] -=f[v];
	if(f[now] > 0) 
	f[v]+=f[now];
}

inline void bfs(int now,int fa)
{
	maxn = max(maxn,f[now]);
	
	for(int v,i=head[now] ;i;i=nxt[i])
	{
		v=ver[i];
		if(v == fa) continue;
		
		recall(now,v);
		bfs(v,now);
		recall(v,now);
	}
}

signed main()
{
	int n;
	
	read(n);
	
	for(int i=1;i<=n;i++) read(point[i]);
	
	for(int x,y,i=1;i<=n-1;i++)
	{
		read(x);
		read(y);
		add(x,y);
		add(y,x);
	}
	
	
	
	dfs(1,0);
	
	bfs(1,0);
	
	cout<<maxn;
}

树的重心

此题的较多性质不再一一分析,
我们着重看以下性质

树的重心必定可以由根节点沿重儿子向下到达

先求出以\(1\)为根沿重儿子向下倍增数组\(f\)
考虑换根法(鬼知道为什么会有人从这想到换根法
每次断边后,分裂出两棵子树,先处理已经一个深度较大的根节点
然后把根换给这个根节点,等到处理完这个子树后回溯回来时,恰好处于另一个深度较低的根节点可以被处理,理所当然的又把根换了回来,同时处理完了断边后的贡献

75pts 做法

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

#define int long long

const int p=299995+5;

int head[p],nxt[2*p],ver[2*p];
int tit;
int size[p];
int depth[p];
int f[p][23];
int fa[p];
int cnt,ans;
int n;

inline void Clear()
{
	memset(fa,0,sizeof(fa));
	memset(depth,0,sizeof(depth));
	memset(f,0,sizeof(f));
	memset(size,0,sizeof(size));
	memset(head,0,sizeof(head));
	tit = 0;
	memset(nxt,0,sizeof(nxt));
	memset(ver,0,sizeof(ver));
	ans = 0;
}

inline void add(int x,int y)
{
	ver[++tit] = y;
	nxt[tit] = head[x];
	head[x] = tit;
}

inline void DFS(int now)
{
	for(int i=head[now] ; i; i=nxt[i])
	{
		int v = ver[i];
		if(depth[v]) continue;
		depth[v] = depth[now] +1;
		DFS(v);
	}
}

inline void dfs(int now,int father)
{
	size[now] = 1;
	int maxn = 0;
	fa[now] = father;
	f[now][0] = now;

	for(int i=head[now],v ; i; i =nxt[i])
	{
		v = ver[i];
		if(v == father) continue;
		dfs(v,now);
		size[now] += size[v];
		if(size[v] > maxn)
		{
			maxn = size[v];
			f[now][0] = v;
		}
	}
	
}

inline void bfs(int now,int father,int &tot)
{
	tot++;
	for(int i=head[now]; i; i=nxt[i])
	{
		if(ver[i] == father) continue;
		bfs(ver[i],now,tot);
	}
}

inline void Init(int a)

{
	for(int i=1;i<=20;i++)
	f[a][i] = f[f[a][i-1]][i-1];

}

inline void recall(int v,int now) 
{
	size[now] = n - size[v];
	size[v] = n;	
	f[v][0] = 0;
	int s = 0;	
	for(int i=head[v],ve;i;i=nxt[i])
	{
		ve= ver[i];
		if(size[ve] > s)
		{
			f[v][0]  =ve;
			s= size[ve];
		}
	}
	
	Init(v);
	
	f[now][0]=0,s=0;
	for(int i=head[now],ve;i;i=nxt[i])
	{
		ve = ver[i];
		if(ve ==v) continue;
		if(size[ve] >s)
		{
			f[now][0] = ve;
			s= size[ve];
		}
	}
	Init(now);
}

inline void dtgh(int now,int father)

{

	for(int i = head[now],xx,sx ; i; i =nxt[i])
	{	
		int v= ver[i];

		xx = v,sx = size[xx];		
		for(int j=20; j>=0;j--)
		{
			if(size[f[xx][j]] > sx - size[f[xx][j]]) 
			{
				xx = f[xx][j];
			}
		} 

		ans+=xx;

		int xxx = f[xx][0];
		
		if(size[xxx] >= sx - size[xxx] && xx != xxx)
		ans +=xxx;
		
		if(v == father) continue;		
		recall(v,now);
		dtgh(v,now);	
		recall(now,v);
	}

}

signed main()

{
	int T;	
	ios_base::sync_with_stdio(false);
	cout.tie(NULL);
	cin.tie(NULL);	
	
	cin>>T;	
	while(T--)
	{		
	cin>>n;
	
	for(int i=1,x,y;i<=n-1;i++)
	{
		cin>>x>>y;
		add(x,y);
		add(y,x);
	}	
	depth[1] = 1;
	DFS(1);
	dfs(1,0);
	
	for(int i=1;i<=20;i++)

		for(int j=1;j<=n;j++)

	f[j][i] = f[f[j][i-1]][i-1]; 

	dtgh(1,0);
	
	cout<<ans<<'\n';
	
	Clear();

	}

很明显我的算法有个致命的缺陷
换根时需要遍历根节点
在随机图下,可以近似认为是一个很小的常数
但若是一个菊花图,则很容易退化成一个\(O(n^2)\)算法

不得不说
倍增重儿子和\(O(logn)\)换根是我没有想到的
设计出这种方法的人真是大佬
轻易做到了我想都不敢想的事情
ai~ , \(mod\ mod \ mod\)

end

最后再说一句
\(CSP RP++\)

剑啊,吸我的血吧!
					      ——《终结的炽天使》

posted @ 2020-11-06 21:41  ·Iris  阅读(272)  评论(0编辑  收藏  举报