【过题记录】 7.28 (树上dp,背包,换根,基环树)

[ZJOI2007] 时态同步


分析:

不难发现,中断点就是叶子节点,
首先,所有叶子节点的高度肯定就等于最深的那个叶子节点的深度。
且不可能去调整最深的叶子结点的深度了。
这样经过一遍dfs之后我们可以计算出每个叶子需要增加的高度。
然后我们发现,如果能调整高度,肯定是要调整尽可能高的那条边,因为这条边调整之后,他所属的叶子节点的深度全部都会加1
于是我们发现,一条边所能调整的最大值,一定是他全部儿子节点所需调整的最小值。
在通过一遍从下往上dfs可以求出每条边调整的最小值
然后再跑一遍从上往下dfs累计答案即可。


Code

#include<bits/stdc++.h>
using namespace std;

#define int long long 

const int N = 5e5+100;
typedef pair < int , int > pii;
#define fi first
#define se second
#define pb push_back
int n;
int rt;
vector < pii > a[N];
int f[N];
int s[N],maxs;
int mins[N];

void Dfs(int x,int faa){
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i].fi,v = a[x][i].se;
		if (y == faa) continue;
		s[y] = s[x]+v;
		Dfs(y,x);
	}
	maxs = max(maxs,s[x]);
}
void Dp1(int x,int faa){
	bool f = 1;
	mins[x] = 1e18;
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i].fi , v = a[x][i].se;
		if (y == faa) continue;
		f = 0;
		Dp1(y,x);
		mins[x] = min(mins[x],mins[y]);
	}
	if (f) mins[x] = maxs-s[x];
}

int ans = 0;
void Dp2(int x,int faa,int sum){
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i].fi; if (y == faa) continue;
		ans+=mins[y]-sum;
		Dp2(y,x,mins[y]);
	}
}

signed main(){
	cin.tie(0);
	ios::sync_with_stdio(false);
	cin>>n>>rt;
	for (int i = 1,x,y,z; i < n; i++)
	  cin>>x>>y>>z,a[x].pb({y,z}),a[y].pb({x,z});
	Dfs(rt,0);
	Dp1(rt,0);
	Dp2(rt,0,0);
	cout<<ans<<endl;
}

有线电视网


分析:

比较明显是一个树上背包问题。
人就相当于是物品,钱就相当于是价值。
f [ i ] [ j ] f[i][j] f[i][j]表示以i为根的树,选j个观众所能获得的最大金币值
可以如下转移(类似分组背包):
f [ x ] [ j ] = m a x ( f [ x ] [ j ] , f [ x ] [ j − i ] + f [ y ] [ i ] − e d . v ) f[x][j] = max(f[x][j],f[x][j-i]+f[y][i]-ed.v) f[x][j]=max(f[x][j],f[x][ji]+f[y][i]ed.v)
答案就是 f [ 1 ] [ i ] > = 0 f[1][i]>=0 f[1][i]>=0的最大i值


Code

#include<bits/stdc++.h>
using namespace std;

const int N = 3e3+100,inf = 3e5+100;
int n,m;
typedef pair < int , int > pii;
#define pb push_back
#define fi first
#define se second

vector < pii > a[N];
int f[N][N];
int v[N];

int Dfs(int x,int faa){
	bool ff = 1;
	int sum = 0;
	f[x][0] = 0;
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i].fi , v = a[x][i].se;
		if (y == faa) continue; ff = 0;
		int t = Dfs(y,x); sum+=t;
		for (int vv = sum; vv >= 1; vv--)
		  for (int j = 1; j <= t; j++)
		    if (vv >= j) f[x][vv] = max(f[x][vv],f[x][vv-j]+f[y][j]-v);
	}
	if (ff){
		f[x][0] = 0;
		f[x][1] = v[x]; return 1;
	}
	return sum;
}

int main(){
	cin.tie(0);
	ios::sync_with_stdio(false);
	cin>>n>>m;
	for (int i = 1; i <= n; i++)
	  for (int j = 1; j <= n; j++) f[i][j] = -inf;
	for (int i = 1; i <= n-m; i++){
		int k; cin>>k;
		for (int j = 1,y,z; j <= k; j++)
		  cin>>y>>z,a[i].pb({y,z}),a[y].pb({i,z});
	}
	for (int i = n-m+1; i <= n; i++) cin>>v[i];
	Dfs(1,0);
	int Max = 0;
	for (int i = n; i >= 0; i--)
	  if(f[1][i]>=0) {
	  	  cout<<i<<endl;
	  	  return 0;
	  }
	return 0; 
}

Nearby Cows G


分析:

题目要我们求离每一个点距离不超过k的点的点权和
其实是一个比较明显的换根dp的题目
这是一颗无根树,但是我们先假设树的根节点为1号点
f [ i ] [ j ] f[i][j] f[i][j]表示以i为根节点,他的子树里距离当前节点不超过j的点权和
于是就有下面很显然的转移方程:
f [ x ] [ j ] = f [ y ] [ j − 1 ] + v [ x ] f[x][j]=f[y][j-1]+v[x] f[x][j]=f[y][j1]+v[x]
这样子跑完一遍dfs之后可以得到以1为根的答案
但是显然不是每个点都得到了答案
所以我们需要跑一遍换根dp,求出每一个点的答案
其实换根dp的关键,就是看当前节点根父亲节点之间的关系,如何通过父亲节点的答案转移到当前子节点的答案
我们这个时候如果 令f的定义中的子树去掉
那么 f [ x ] [ k ] f[x][k] f[x][k]显然就是每个点的答案。
那么我们如何转移,才能将上面的定义转化为这个定义呢?
关键就是找出非子树内的距离当前点不超过k的点的权值和是多少
我们发现这个可以通过父亲来转移
f [ y ] [ k ] + = f [ x ] [ k − 1 ] f[y][k]+=f[x][k-1] f[y][k]+=f[x][k1]
但是我们发现这个时候会有一点重复,因为 f [ x ] [ k − 1 ] f[x][k-1] f[x][k1]有一部分答案是包含当前子树的,而当前子树的答案我们已经有了
所以还需要减去 f [ y ] [ k − 2 ] f[y][k-2] f[y][k2]
综上, f [ y ] [ k ] + = f [ x ] [ k − 1 ] − f [ y ] [ k − 2 ] f[y][k]+=f[x][k-1]-f[y][k-2] f[y][k]+=f[x][k1]f[y][k2]


#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N = 1e5+100;
int f[N][30];
int n,k;
vector < int > a[N];
int ans[N];
#define pb push_back
#define fi first
#define se second
int v[N];

void Insert(int x,int y){
	a[x].pb(y);
}

void Dfs(int x,int faa){
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i]; if (y == faa) continue;
		Dfs(y,x);
		for (int kk = 1; kk <= k; kk++)
		  f[x][kk]+=f[y][kk-1];
	}
	for (int i = 0; i <= k; i++) f[x][i]+=v[x];
}

void Dp(int x,int faa){
	ans[x] = f[x][k];
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i]; if (y == faa) continue;
		for (int kk = k; kk >= 1; kk--){
			f[y][kk]+=f[x][kk-1];
			if (kk >= 2) f[y][kk]-=f[y][kk-2];
		}
		Dp(y,x);
	}
}

signed main(){
	cin.tie(0);
	ios::sync_with_stdio(0);
	cin>>n>>k;
	for (int i = 1,x,y; i < n; i++)
	  cin>>x>>y,Insert(x,y),Insert(y,x);
	for (int i = 1; i <= n; i++) cin>>v[i];
	Dfs(1,0);
	Dp(1,0);
	for (int i = 1; i <= n; i++) cout<<ans[i]<<endl;
	return 0;
}

发现环


分析:

基环树找环
用拓扑排序去找环

Code

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+100;
int du[N];
vector < int > a[N];
#define pb push_back
int n;
bool vi[N];

int main(){
	cin>>n;
	for (int i = 1,x,y; i <= n; i++)
	  cin>>x>>y,a[x].pb(y),a[y].pb(x),du[x]++,du[y]++;
	queue < int > q;
	for (int i = 1; i <= n; i++)
	  if (du[i] == 1) q.push(i);
	while (q.size()){
		int x = q.front(); q.pop();
		vi[x] = 1;
		for (int i = 0; i < a[x].size(); i++){
			int y = a[x][i];
			du[y]--;
			if (du[y] == 1) q.push(y);
		}
	}
	for (int i = 1; i <= n; i++)
	  if (!vi[i]) cout<<i<<' ';
	return 0;
}

城市环路


分析:

这道题其实本来应该是一个基环树dp的题
但是这边没采用这个方法
其实我们发现,隔一个人选一个点,有点类似于奇偶性
只能选取奇偶性相同的点。
那么对于环上的点,其实我们只需要找出任意两个相连的点,将两个点分别作为树的根跑一遍树形dp即可
f [ i ] [ 0 / 1 ] f[i][0/1] f[i][0/1]表示选/不选当前点的最优答案
那么最终答案就是 m a x ( f [ x ] [ 0 ] , f [ y ] [ 0 ] ) max(f[x][0],f[y][0]) max(f[x][0],f[y][0])
限定两个点分别不选,是因为其实这两个点在环上是连着的,这样一定满足条件。
找出在环上的任意两个点可以采用并查集
最后一个加边使形成一个环,此时两个点一定处在同一集合


#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N = 1e6+100;
int fa[N];
vector < int > a[N];
#define pb push_back
int n;
int v[N];
int f[N][3];
double k;

int getfa(int x){
	return fa[x] == x?x:fa[x] = getfa(fa[x]);
}

void Dp(int x,int faa){
	bool ff = 1;
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i]; if (y == faa) continue;
		ff = 0;
		Dp(y,x);
		f[x][1]+=f[y][0];
		f[x][0]+=max(f[y][1],f[y][0]);
	}
	f[x][1]+=v[x];
}

signed main(){
	cin.tie(0);
	ios::sync_with_stdio(false);
	cin>>n;
	for (int i = 1; i <= n; i++) fa[i] = i;
	int st,ed;
	for (int i = 1; i <= n; i++){
		int x,y; cin>>v[i]>>y;
		int X = getfa(i) , Y = getfa(y);
		if (X == Y){
			st = i; ed = y; continue;
		}
		a[i].pb(y); a[y].pb(i);
		fa[X] = Y;
	}
	Dp(st,0);
	int ans = f[st][0];
	memset(f,0,sizeof f);
	Dp(ed,0);
	ans = max(ans,f[ed][0]);
	cout<<ans;
	return 0;
}

骑士


分析:

这道题跟上一题基本一样
但是需要注意的是这道题可能是基环树森林
对于森林里的每个数都累加一遍答案即可

Code

#include<bits/stdc++.h>
using namespace std;

#define int long long

const int N = 1e6+100;
int fa[N];
vector < int > a[N];
#define pb push_back
int n;
int v[N];
int f[N][3];
bool vi[N];
int st[N],ed[N];
int cnt = 0;

int getfa(int x){
	return fa[x] == x?x:fa[x] = getfa(fa[x]);
}

vector < int > live;
void Dp(int x,int faa){
	live.pb(x);
	bool ff = 1;
	for (int i = 0; i < a[x].size(); i++){
		int y = a[x][i]; if (y == faa) continue;
		ff = 0;
		Dp(y,x);
		f[x][1]+=f[y][0];
		f[x][0]+=max(f[y][1],f[y][0]);
	}
	f[x][1]+=v[x];
}

signed main(){
	cin.tie(0);
	ios::sync_with_stdio(false);
	cin>>n;
	for (int i = 1; i <= n; i++) fa[i] = i;
//	int st,ed;
	for (int i = 1; i <= n; i++){
		int x,y; cin>>v[i]>>y;
		int X = getfa(i) , Y = getfa(y);
		if (X == Y){
			st[++cnt] = i; ed[cnt] = y; continue;
//			st = i; ed = y; continue;
		}
		a[i].pb(y); a[y].pb(i);
		fa[X] = Y;
	}
	int ans = 0;
	for (int i = 1; i <= cnt; i++){
		Dp(st[i],0);
		int now = f[st[i]][0];
		for (int j = 0; j < live.size(); j++) f[live[j]][0] = f[live[j]][1] = 0;
		live.clear();
		Dp(ed[i],0); now = max(now,f[ed[i]][0]);
		for (int j = 0; j < live.size(); j++) f[live[j]][0] = f[live[j]][1] = 0;
		live.clear();
		ans+=now;
	}
	cout<<ans;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值