题目大意
给定一个有向图无环图\(G\),在这个图中寻找一条路径,是这条路径上的点权所组成的序列的最长不下降子序列的长度最长。
思路部分
解决无后效性
求一个有向无环图中的最长不下降子序列,不难想到这应该是一个图上dp,而在一个图中我们没法保证编号\(1\)到\(N\)去计算一定是满足了无后效性的,所以我们考虑找到一个顺序,这个顺序满足无后效性。
而在有向无环图中寻找一个序列,这个序列满足对于任意\(i(1≤i≤n)\),小于\(i\)的所有节点都在\(i\)的左边(或一样),大于\(i\)的所有节点都在\(i\)的右边(或一样)。
根据上述定义,不难看出这是这个有向图的拓扑序列。拓扑序的求法如下:
- 找到入度为\(0\)的点,入队。
- 重复执行直到队列为空,从队首取出一个元素\(u\),枚举这个点能够到达的点\(v\),把点\(v\)的入度减一,判断如果点\(v\)的入度为0,那么入队。
在求拓扑序时,已经满足了无后效性这个条件,所以我们直接在这个过程中对答案求解。
解决动态规划
接着,我们讨论该如何对答案进行求解。
考虑在线性元素中对最长不下降子序列求解的定义为:以第\(i\)个元素为结尾的最长长度。那么我们是否能再次运用这个定义呢?
答案是肯定不行 。第一:时间复杂度接受不了\(o(N^2)\)会炸。第二:我们无法知道以更加前面的节点为结尾的最长长度。
那么我们该如何求解呢?
根据数据范围可知,虽然点和边的数量很大,但是每个节点的点权很小,最大也就才\(10\)。所以我们可以想出以节点\(i\),点权为\(j\)结尾的最长子序列。这时,我们不需要直到之前的节点了,只需要枚举颜色是什么即可。而转移方程类似于背包,代码入下:
cpp
for(int i = 1; i <= n; i++)
if(!in[i]){//寻找入度为0的点
q.push(i);
f[i][a[i]] = 1;//对dp数组初始化
}
while(!q.empty()){//寻找拓扑序
int x = q.front(); q.pop();//队首出队
for(auto y : mp[x]){//枚举它能到达的点
for(int i = 1; i <= 10; i++){
f[y][i] = max(f[y][i], f[x][i]);//直接转移
if(a[y] >= i) //如果我的点权大于等于i,那我就可以把长度增加以(在f[x][i]的基础上)
f[y][a[y]] = max(f[y][a[y]], f[x][i] + 1);//更新
}
if(-- in[y] == 0)//寻找入度为0的点,入队
q.push(y);
}
}
这就是这个题的核心代码,读者看到这就可以自行实现一下了!但如果还是不会,那么完整代码入下。
code
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, A = 20;
int n, m, f[N][A], ans, in[N], a[N];
vector <int> mp[N];
queue <int> q;
int main(){
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
while(m --){
int u, v; scanf("%d %d", &u, &v);
mp[u].push_back(v);
in[v] ++;
}
for(int i = 1; i <= n; i++)
if(!in[i]){
q.push(i);
f[i][a[i]] = 1;
}
while(!q.empty()){
int x = q.front(); q.pop();
for(auto y : mp[x]){
for(int i = 1; i <= 10; i++){
f[y][i] = max(f[y][i], f[x][i]);
if(a[y] >= i)
f[y][a[y]] = max(f[y][a[y]], f[x][i] + 1);
}
if(-- in[y] == 0)
q.push(y);
}
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= 10; j++)
ans = max(ans, f[i][j]);
printf("%d\n", ans);
return 0;
}