基环树上dp

文章介绍了如何利用树形动态规划和线性动态规划解决基环树上的问题,如ZJOI2008年的‘骑士’题目所示。首先找到图中的环,然后以环上的每个节点为根进行树形DP,接着断环成链,对环进行线性DP。在具体实现中,涉及到了如何计算不同选法下的最大战斗力,并给出了相应的代码实现。

对于任意一棵基环树,它的长相是这样的

基环树

先找到图中的环

基环树

然后对于环上的每一个节点为根,对其子树进行树形dp,最后断环成链,对环进行线性dp,

image-20230222171425101

以下面的题目为例

[ZJOI2008] 骑士

题目描述

Z 国的骑士团是一个很有势力的组织,帮会中汇聚了来自各地的精英。他们劫富济贫,惩恶扬善,受到社会各界的赞扬。

最近发生了一件可怕的事情,邪恶的 Y 国发动了一场针对 Z 国的侵略战争。战火绵延五百里,在和平环境中安逸了数百年的 Z 国又怎能抵挡的住 Y 国的军队。于是人们把所有的希望都寄托在了骑士团的身上,就像期待有一个真龙天子的降生,带领正义打败邪恶。

骑士团是肯定具有打败邪恶势力的能力的,但是骑士们互相之间往往有一些矛盾。每个骑士都有且仅有一个自己最厌恶的骑士(当然不是他自己),他是绝对不会与自己最厌恶的人一同出征的。

战火绵延,人民生灵涂炭,组织起一个骑士军团加入战斗刻不容缓!国王交给了你一个艰巨的任务,从所有的骑士中选出一个骑士军团,使得军团内没有矛盾的两人(不存在一个骑士与他最痛恨的人一同被选入骑士军团的情况),并且,使得这支骑士军团最具有战斗力。

为了描述战斗力,我们将骑士按照 111nnn 编号,给每名骑士一个战斗力的估计,一个军团的战斗力为所有骑士的战斗力总和。

输入格式

第一行包含一个整数 nnn,描述骑士团的人数。

接下来 nnn 行,每行两个整数,按顺序描述每一名骑士的战斗力和他最痛恨的骑士。

输出格式

应输出一行,包含一个整数,表示你所选出的骑士军团的战斗力。

样例 #1

样例输入 #1

3
10 2
20 3
30 1

样例输出 #1

30

提示

数据规模与约定

对于 30%30\%30% 的测试数据,满足 n≤10n \le 10n10

对于 60%60\%60% 的测试数据,满足 n≤100n \le 100n100

对于 80%80\%80% 的测试数据,满足 n≤104n \le 10 ^4n104

对于 100%100\%100% 的测试数据,满足 1≤n≤1061\le n \le 10^61n106,每名骑士的战斗力都是不大于 10610^6106 的正整数。

思路

由题意得,这道题是一个基环树森林,所以拆成每一个基环树来做。

以环上的每一个点为根做树形dp,设dp1[x][0/1]dp1[x][0/1]dp1[x][0/1]表示在以节点xxx为根的子树内,不选或者选点xxx的最大攻击力。设yyyxxx在环外的子节点,那么明显方程为
dp1[x][0]=∑y∈sonmax(dp1[y][0],dp1[y][1])dp1[x][1]=(∑y∈sondp1[y][1])+weight[x] dp1[x][0]=\sum_{y\in son} max(dp1[y][0], dp1[y][1])\\ dp1[x][1]=(\sum_{y\in son}dp1[y][1])+weight[x] dp1[x][0]=ysonmax(dp1[y][0],dp1[y][1])dp1[x][1]=(ysondp1[y][1])+weight[x]
断环成链,对环上的点进行线性dp,注意需要枚举端点选或不选的情况,如果起点有士兵,则终点不能放士兵;否则终点可放可不放士兵,cyc[j]cyc[j]cyc[j]对应环上的结点编号
dp2[j][0]=max(dp2[j−1][0],dp2[j−1][1])+dp1[cyc[j]][0]dp2[j][1]=dp2[j−1][0]+dp1[cyc[j]][1] dp2[j][0] = max(dp2[j - 1][0], dp2[j - 1][1]) + dp1[cyc[j]][0]\\ dp2[j][1] = dp2[j - 1][0] + dp1[cyc[j]][1] dp2[j][0]=max(dp2[j1][0],dp2[j1][1])+dp1[cyc[j]][0]dp2[j][1]=dp2[j1][0]+dp1[cyc[j]][1]
起点有士兵时
ans=max(ans,dp2[m−1][0]) ans = max(ans, dp2[m - 1][0]) ans=max(ans,dp2[m1][0])
起点没有士兵时
ans=max(dp2[m−1][0],dp2[m−1][1]) ans = max(dp2[m - 1][0], dp2[m - 1][1]) ans=max(dp2[m1][0],dp2[m1][1])

#include<iostream>
#include<string>
#include<vector>
#include<queue>
#include<unordered_map>
#include<unordered_set>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<random>
#include<ctime>
#include<cstring>
#include<cstdio>
#include<cstring>
#include<bitset>
using namespace std;
const int N = 1e6 + 5;
typedef long long ll;
int w[N], fa[N], vis[N], oncyc[N];
ll dp1[N][2], dp2[N][2];
vector<vector<int>> e;
void dfs(int x) {
	// 选择该点,加上权值
	dp1[x][1] = w[x];
	vis[x] = 1;
	for (int i = 0; i < e[x].size(); i++) {
		int v = e[x][i];
		if (oncyc[v]) continue;
		dfs(v);
		dp1[x][0] += max(dp1[v][0], dp1[v][1]);
		dp1[x][1] += dp1[v][0];
	}
}
int main()
{
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	int n;
	ll res = 0;
	cin >> n;
	e.resize(n + 1);
	for (int i = 1; i <= n; i++) {
		cin >> w[i] >> fa[i];
		e[fa[i]].push_back(i);
	}
	for (int i = 1; i <= n; i++) {
		if (vis[i]) continue;
		int cur = i;
		// 找到环上的一点
		while (!vis[cur]) {
			vis[cur] = 1;
			cur = fa[cur];
		}
		vector<int> cyc;
		int p = cur;
		// 记录环上所有点
		while (1) {
			cyc.push_back(p);
			oncyc[p] = 1;
			p = fa[p];
			if (p == cur) break;
		}
		// 对环上所有点及其环外孩子进行树形dp,注意树形dp要排除掉cyc[i]在环上的孩子结点
		for (int j = 0; j < cyc.size(); j++) {
			dfs(cyc[j]);
		}
		int m = cyc.size();
		memset(dp2, 0, sizeof(dp2));
		// 断环成链,枚举起点是否有士兵
		// 起点没有士兵
		dp2[0][0] = dp1[cyc[0]][0];
		for (int j = 1; j < m; j++) {
			dp2[j][0] = max(dp2[j - 1][0], dp2[j - 1][1]) + dp1[cyc[j]][0];
			dp2[j][1] = dp2[j - 1][0] + dp1[cyc[j]][1];
		}
		ll ans = max(dp2[m - 1][0], dp2[m - 1][1]);
		memset(dp2, 0, sizeof(dp2));
		// 起点有士兵
		dp2[0][1] = dp1[cyc[0]][1];
		for (int j = 1; j < m; j++) {
			dp2[j][0] = max(dp2[j - 1][0], dp2[j - 1][1]) + dp1[cyc[j]][0];
			dp2[j][1] = dp2[j - 1][0] + dp1[cyc[j]][1];
		}
		ans = max(ans, dp2[m - 1][0]);
		res += ans;
	}
	cout << res;
	return 0;
}

城市环路

题目背景

一座城市,往往会被人们划分为几个区域,例如住宅区、商业区、工业区等等。

B 市就被分为了以下的两个区域——城市中心和城市郊区。在这两个区域的中间是一条围绕 B 市的环路,环路之内便是 B 市中心。

题目描述

整个城市可以看做一个 nnn 个点,nnn 条边的单圈图(保证图连通),唯一的环便是绕城的环路。保证环上任意两点有且只有 222 条简单路径互通。图中的其它部分皆隶属城市郊区。

现在,有一位名叫 Jim 的同学想在 B 市开店,但是任意一条边的 222 个点不能同时开店,每个点都有一定的人流量,第 iii 个点的人流量是 pip_ipi,在该点开店的利润就等于 pi×kp_i×kpi×k,其中 kkk 是一个常数。

Jim 想尽量多的赚取利润,请问他应该在哪些地方开店?

输入格式

第一行一个整数 nnn,代表城市中点的个数。城市中的 nnn 个点由 0∼n−10 \sim n-10n1 编号。

第二行有 nnn 个整数,第 (i+1)(i + 1)(i+1) 个整数表示第 iii 个点的人流量 pip_ipi

接下来 nnn 行,每行有两个整数 u,vu, vu,v,代表存在一条连接 uuuvvv 的道路。

最后一行有一个实数,代表常数 kkk

输出格式

输出一行一个实数代表答案,结果保留一位小数。

样例 #1

样例输入 #1

4
1 2 1 5
0 1
0 2
1 2
1 3
2

样例输出 #1

12.0

提示

数据规模与约定
  • 对于 20%20\%20% 的数据,保证 n≤100n \leq 100n100
  • 另有 20%20\%20% 的数据,保证环上的点不超过 200020002000 个。
  • 对于 100%100\%100% 的数据,保证 1≤n≤1051 \leq n \leq 10^51n1051≤pi≤1041 \leq p_i \leq 10^41pi1040≤u,v<n0 \leq u, v < n0u,v<n0≤k≤1040 \leq k \leq 10^40k104kkk 的小数点后最多有 666 位数字。

思路

跟骑士那题本质相同,但本题只有一棵基环树,且是无向图,需要dfs找环。然后进行环外树形dp,环上线性dp即可

#include<iostream>
#include<string>
#include<vector>
#include<queue>
#include<unordered_map>
#include<unordered_set>
#include<map>
#include<set>
#include<algorithm>
#include<cmath>
#include<random>
#include<ctime>
#include<cstring>
#include<cstdio>
#include<cstring>
#include<bitset>
using namespace std;
const int N = 1e5 + 5;
typedef long long ll;
int p[N], vis[N], oncyc[N];
ll dp1[N][2], dp2[N][2];
vector<int> stk, cyc;
vector<vector<int>> e;
bool dfs1(int x, int fa) {
	if (vis[x]) {
		cyc.push_back(x);
		oncyc[x] = 1;
		while (stk.back() != x) {
			cyc.push_back(stk.back());
			oncyc[stk.back()] = 1;
			stk.pop_back();
		}
		return true;
	}
	stk.push_back(x);
	vis[x] = 1;
	for (int i = 0; i < e[x].size(); i++) {
		int v = e[x][i];
		if (v == fa) continue;
		if (dfs1(v, x)) return true;
	}
	stk.pop_back();
	return false;
}
void dfs2(int x, int fa) {
	dp1[x][1] = p[x];
	for (int i = 0; i < e[x].size(); i++) {
		int v = e[x][i];
		if (v == fa) continue;
		if (oncyc[v]) continue;
		dfs2(v, x);
		dp1[x][1] += dp1[v][0];
		dp1[x][0] += max(dp1[v][1], dp1[v][0]);
	}
}
int main()
{
	cin.tie(0), cout.tie(0);
	ios::sync_with_stdio(0);
	int n;
	cin >> n;
	e.resize(n); 
	for (int i = 0; i < n; i++) cin >> p[i];
	for (int i = 0; i < n; i++) {
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	double k;
	cin >> k;
	dfs1(0, -1);
	int m = cyc.size();
	for (int i = 0; i < m; i++) {
		dfs2(cyc[i], -1);
	}
	dp2[0][0] = dp1[cyc[0]][0];
	for (int i = 1; i < m; i++) {
		dp2[i][0] = max(dp2[i - 1][0], dp2[i - 1][1]) + dp1[cyc[i]][0];
		dp2[i][1] = dp2[i - 1][0] + dp1[cyc[i]][1];
	}
	ll ans = max(dp2[m - 1][0], dp2[m - 1][1]);
	dp2[0][0] = 0, dp2[0][1] = dp1[cyc[0]][1];
	for (int i = 1; i < m; i++) {
		dp2[i][0] = max(dp2[i - 1][0], dp2[i - 1][1]) + dp1[cyc[i]][0];
		dp2[i][1] = dp2[i - 1][0] + dp1[cyc[i]][1];
	}
	ans = max(dp2[m - 1][0], ans);
	k *= ans;
	printf("%.1f", k);
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值