Codeforces Round #791 (Div. 2) D - Toss a Coin to Your Graph...(树形 dp + 二分 + dfs 判环)

这是一道很值得细细回味的好题目

题目来源

D Toss a Coin to Your Graph…

点此进入题面

题意:

给定一个有向图,每次可以从任意节点出发找一条 长度为 k - 1 的路径(即 总节点数为 k),要求,求出路径上点权最大值最小是多少。

思路:

这道题的基本思路是 二分答案 + 树形 dp,根据图的特性重点放在 设计 dfs 操作

首先我们可以得出结论:

  • 若所选的点权值越大,越容易符合 k - 1 条边(因为 选择的答案越大,显然 小于等于这个答案的点权就越多,就越容易构造出 k - 1 条边),

  • 反之,越难符合 k - 1 条边

因此,答案满足单调性,并且如果要 最小化最大值,我们可以考虑用 二分来查找最大点权值,并最小化它

而对于我们 每次二分的答案最大点权值 mid,需要 check(mid) 一下,看看是否满足题目条件

二分的答案区间 左闭右闭循环条件 l <= rl 来记录答案,若 mid 满足条件,则 r = mid 否则 l = mid + 1 寻找有无更小的 mid(这是 二分查找最小值的套路做法

对于 check 函数点的权值不能超过二分的 mid 且如果存在某一条路径点数 >= k 返回 true核心 I

对于 dfs:(核心 II

有向图,可以 有环,所以当我们在 dfs 的过程中 遇到一个合法环就可以直接判为合法路径(一下子用完 k 步),所以需要 判环操作

  • 关于 判环 的具体操作请见 dfs 代码

无环情况下 我们要找的显然是 一条链,所以还需要 统计从某个节点出发的最长链

  • 所以我们还需要 编写 dfs 函数,回溯的时候进行 dp状态转移

  • dp[i] 状态表示从节点 i 开始的最长合法路径的总节点数

  • dp[i] 状态计算dp[i] = max(dp[i], dp[s1] + 1, dp[s2] + 1, ..., dp[sn] + 1)s1、s2、...、sn节点 i 的所有子节点

dfs 选择节点走时 必要条件是 选择的这个节点的权值 小于等于 我们想要的二分值(紧扣二分答案的定义即可)

dfs 之后 判断 是否存在合法路径,也就是说 是否存在以某个点为起点的(最长)合法路径长度 >= k

更多细节详见代码注释。

时间复杂度:

O ( ( n + m ) l o g M A X ) O((n+m)logMAX) O((n+m)logMAX)

代码:

#define _CRT_SECURE_NO_WARNINGS 1
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")

#include<bits/stdc++.h>

using namespace std;

#define int long long
typedef long long ll;
typedef unsigned long long ull;
typedef vector<int> vi;
typedef pair<int, int> pii;
typedef map<int, int> mi;
typedef vector<ll> vll;
typedef pair<ll, ll> pll;
#define pb push_back
#define pp pop_back
#define v first
#define y second
const int N = 2e5 + 10;
bool st[N];		//判断是否出现环
bool vis[N];	//判断节点是否访问过
int dp[N];		//dp[i] 状态表示:从节点 i 开始的最长合法路径的总节点数
int h[N], e[N], ne[N], w[N], idx;
int n, m, k;

void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void dfs(int u, int v)
{
	st[u] = vis[u] = true;
	dp[u] = 1;	//当前节点算一个
	for (int i = h[u]; ~i; i = ne[i])
	{
		int j = e[i];
		if (w[j] <= v)		//即将遍历的子节点权值要小于等于当前二分答案 v
		{
			if (st[j]) dp[u] = k;	//又访问已经经过的节点,说明形成了环
			else
			{
				if (!vis[j]) dfs(j, v);		//没有走过的节点继续选择
				dp[u] = max(dp[u], dp[j] + 1);	//回溯时进行状态转移
			}
		}
	}
	st[u] = false;	//恢复现场为了找其它的环
}

bool check(int v)
{
	memset(vis, false, sizeof vis);
	for (int i = 1; i <= n; ++i) if (!vis[i] && w[i] <= v) dfs(i, v);	//开始搜索要保证点权都小于等于当前二分的答案(这就保证了我们是求解最大值的最小情况)
	for (int i = 1; i <= n; ++i) if (vis[i] && dp[i] >= k) return true;	//只要存在从节点 i 开始的最长合法路径的长度是大于等于 k 的,则说明当前二分的答案是符合要求的
	return false;
}

signed main()
{
	int T = 1;
	//cin>>T;

	while (T--)
	{
		cin >> n >> m >> k;

		for (int i = 1; i <= n; ++i) scanf("%lld", &w[i]);

		memset(h, -1, sizeof h);
		while (m--)
		{
			int a, b;
			scanf("%lld%lld", &a, &b);
			add(a, b);
		}

		//接下来二分答案:求解最小值
		int l = 1, r = 1e9 + 1;		
		while (l < r)
		{
			int mid = l + r >> 1;
			if (check(mid)) r = mid;
			else l = mid + 1;
		}

		if (l == 1e9 + 1) puts("-1");	//未找到答案返回1e9+1,类似于 end()
		else printf("%lld\n", l);
	}

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值