这是一道很值得细细回味的好题目
D Toss a Coin to Your Graph…
点此进入题面
题意:
给定一个有向图,每次可以从任意节点出发找一条 长度为 k - 1
的路径(即 总节点数为 k
),要求,求出路径上点权最大值最小是多少。
思路:
这道题的基本思路是 二分答案 + 树形 dp
,根据图的特性重点放在 设计 dfs
操作
首先我们可以得出结论:
-
若所选的点权值越大,越容易符合
k - 1
条边(因为 选择的答案越大,显然 小于等于这个答案的点权就越多,就越容易构造出k - 1
条边), -
反之,越难符合
k - 1
条边
因此,答案满足单调性,并且如果要 最小化最大值,我们可以考虑用 二分来查找最大点权值,并最小化它
而对于我们 每次二分的答案:最大点权值 mid
,需要 check(mid)
一下,看看是否满足题目条件
二分的答案区间 左闭右闭,循环条件 l <= r
,用 l
来记录答案,若 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;
}