树形规划 换根法
也不知道自己之前颓来颓去的都学了写啥
简析
换根法
主要解决以不同点为根进行动态规划,进而求得最优解的一类题目的解法
这种题目能够从朴素解法中找到突破口,往往是发现根的子节点与根之间的关系,进而能够O(1)换根,使得整个算法的复杂度可以降低一个维度
根节点与子节点的交换是本解法的核心
题目
[USACO10MAR]Great Cow Gathering G
题目
初看此题,很容易想到一种\(O(n^{2})\)的朴素解法
依次枚举根节点再
进行一遍根结点的单源最短路,再枚举每个节点计算贡献
但是这种写法是对正解的引导是很弱的,我们可以换一种方向考虑
\(f[x]\)维护x的子树到\(x\)的贡献,相当于我们已经把\(x\)子树中所有的权值都集中于\(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\)的情况下最大指数和
由此方程得,最终求得的最优解必然受到\(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++\)
剑啊,吸我的血吧!
——《终结的炽天使》