【拓扑排序+动态规划】例题及解题报告

1 模版介绍

拓扑排序+动态规划结合使用可以求有向无环图的最长路或最短路。

  1. 数据存储:
    · din数组:存储各顶点入度数。
    · q队列:让入度为0的点入队,进行拓扑排序。
    · score数组:存储以顶点v为终点的最值。
    · ans变量:存储最长路或最短路的值。
    · G数组:邻接表(也可以用链式前向星)。
    · W数组:与G数组下标对应,存储权值。

  2. 算法过程:
    step 1:输入各边的起点、终点和权值。更新数组din、G和W。
    step 2:如果求最长路,初始化score数组元素为一个较小的值(求最长路反之)。
    step 3:通过判断din的值,让入度为0的点进入队列。
    step 4:使用拓扑排序求最值:
    1)获取队头元素存在ft中,队头元素出队。
    2)用循环变量i遍历邻接表G[ft],令din[G[ft][i]]–。更新ans的值为min(ans,score[ft]+W[ft][i])。同时也要对score[G[ft][i]]做更新,score[G[ft][i]]=min(score[G[ft][i]],score[ft]+W[ft][i])。
    3)判断如果G[ft][i]的入度为0,则该点入队
    一直这样做,直到队列为空。
    step 5:输出ans。

  3. 算法解释:
    求最值我们常用动态规划,动态规划要有最优子结构,并且无后效性,为了做到这一点,我们可以用拓扑排序把先遍历到的节点最值确定再扩展到后续节点。
    ① 我们首先让入度为0的点入队,保证起点合法。
    ② 在扩展路径的过程中,我们通过遍历队头元素的邻接表,把所有可能的点都试一遍,如果发现当前节点最值加上邻接的点的值大于当前最值ans,则更新ans。同时,更新邻接的点的最值,以便后续用该点扩展的最值也是正确的。
    ③ 当一个点v的入边全部被遍历后,score[v]便得到了以v为终点的最值。我们这时把点v加入队列,继续扩展。

例题

例题1 洛谷P1807

解题思路(参考洛谷题解)

这道题属于模版题,但是要注意一下题目细节:
在这里插入图片描述
输入格式里面说了“每行 3 个整数 u,v,w(u<v),代表存在一条从 u 到 v 边权为 w 的边”,可以知道1号点是没有入边的,因为没有编号小于1。还有一个要注意的点是,可能有除了1号点外入度为0的点,而题目要我们求1,n间的最长路径,所以起点只能是1号点,其他入度为0的点无法抵达,我们要先用拓扑排序把这些点相邻的节点的度数减去。然后节点1入队,再用一次拓扑排序去求最值。

AC代码
#include<bits/stdc++.h>
#define maxn 1600
#define inf 0x3f3f3f3f
using namespace std;
long long ans=-inf,n,m,din[maxn],score[maxn];
queue<int> q;
vector<int> G[maxn],W[maxn];
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		G[u].push_back(v);
		W[u].push_back(w);
		din[v]++;
	}
	for(int i=2;i<=n;i++) {
		score[i]=-inf;
		if(din[i]==0) 
			q.push(i);	
	}
	while(q.size()){
		int ft=q.front();
		q.pop();
		for(int i=0;i<G[ft].size();i++){
			if(--din[G[ft][i]]==0) q.push(G[ft][i]);			
		}
	}
	q.push(1);
	while(q.size()){
		int ft=q.front();
		q.pop();
		for(int i=0;i<G[ft].size();i++){
			din[G[ft][i]]--;
			score[G[ft][i]]=score[G[ft][i]]>score[ft]+W[ft][i]?score[G[ft][i]]:score[ft]+W[ft][i];	
			if(din[G[ft][i]]==0)
				q.push(G[ft][i]);
		}
	}
	if(score[n]==-inf) cout<<-1;
	else cout<<score[n];
    return 0;
}

例题2:洛谷P10166

题目链接

解题思路(参考洛谷题解)

这道题加边思路是:1.找两个不相邻的点加两条边;2.找相邻的点加一条边。加两条边的话,我们最后就是找序列最小的两个顶点加边,最值存在ans中。后面再通过拓扑排序,去看看有没有相邻的节点加一条边代价小于ans,如果有则更新ans。

AC代码
#include<bits/stdc++.h>
#define maxn 1000005
#define ll long long 
using namespace std;
struct edge{
	ll u,v,nt;
}e[maxn];
queue<ll> q;
ll m,n,a[maxn],ans,dp[maxn],cnt,din[maxn],h[maxn];
void add(int a,int b){
	++cnt;
	e[cnt].u=a;
	e[cnt].v=b;
	e[cnt].nt=h[a];
	h[a]=cnt;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		dp[i]=a[i];
	}
	ll x=0x3f3f3f3f,y=0x3f3f3f3f;
	for (int i = 1; i <= n; i++) {
		if (a[i] <= x) y = x, x = a[i];
		else if (a[i] < y) y = a[i];
	}
	ans = x + y << 1;
	for(int i=1;i<=m;i++){
		int u,v;
		cin>>u>>v;
		add(u,v);
		din[v]++;
	}
	for(int i=1;i<=n;i++){
		if(din[i]==0) q.push(i);
	}
	while(q.size()){
		ll ft=q.front();
		q.pop();
		for(int i=h[ft];i;i=e[i].nt){
			int u=e[i].u,v=e[i].v;
			din[v]--;
			ans=ans<dp[u]+a[v]?ans:dp[u]+a[v];
			dp[v]=dp[v]<dp[u]?dp[v]:dp[u];
			if(din[v]==0) q.push(v);
		}
	}
	for(int i=1;i<=n;i++){
		if(din[i]) ans=0;
	}
	cout<<ans;
    return 0;
}

例题3 洛谷P3387

题目链接

解题思路(参考洛谷题解)

使用tarjan算法求强连通分量,之后把每个强连通分量当成一个点建图,使用拓扑排序+dp求解最值。

AC代码
#include <bits/stdc++.h>
#define maxn 100005
using namespace std;
int cnt, m, n, dfn[maxn], a[maxn], low[maxn], val_color[maxn], color[maxn], num_color;
int Din[maxn],mm[10005][10005],score[maxn],cc[maxn];
bool vis[maxn], vc[maxn];
stack<int> s;
vector<int> G[maxn],Dout[maxn];
queue<int> q;
void tarjan(int x) {
	cnt++;
	dfn[x] = low[x] = cnt;
	vis[x] = 1;
	s.push(x);
	for (int i = 0; i < G[x].size(); i++) {
		int q = G[x][i];
		if (dfn[q] == 0) {
			tarjan(q);
			low[x] = min(low[x], low[q]);
		} else if (vis[q])
			low[x] = min(low[x], dfn[q]);
	}
	if (dfn[x] == low[x]) {
		int t = s.top();
		num_color++;
		while (t != x) {
			cc[num_color]++;
			s.pop();
			color[t] = num_color;
			val_color[num_color] += a[t];
			vis[t] = 0;
			t = s.top();
		}
		s.pop();
		color[x] = num_color;
		cc[num_color]++;
		val_color[num_color] += a[x];
		vis[x] = 0;
	}
}

int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		G[u].push_back(v);
	}
	for (int i = 1; i <= n; i++) {
		if (dfn[i] == 0) {
			tarjan(i);
		}
	}
	for (int j = 1; j <= n; j++) {
			for (int k = 0; k < G[j].size(); k++) {
				if (color[j] != color[G[j][k]]&&mm[color[G[j][k]]][color[j]]==0){
					Din[color[G[j][k]]]++;
					Dout[color[j]].push_back(color[G[j][k]]);	
					mm[color[j]][color[G[j][k]]]=1;
					mm[color[G[j][k]]][color[j]]=1;
				} 
			}
	}
	int ans=0;
	for (int i = 1; i <= num_color; i++) {
		score[i]=val_color[i];
		ans=max(ans,score[i]);
		if(Din[i]==0) q.push(i);
	}
	while(q.size()){
		int ft=q.front();
		q.pop();
		for(int i=0;i<Dout[ft].size();i++){
			ans=max(ans,score[ft]+val_color[Dout[ft][i]]);
			score[Dout[ft][i]]=max(score[Dout[ft][i]],score[ft]+val_color[Dout[ft][i]]);
			Din[Dout[ft][i]]--;
			if(Din[Dout[ft][i]]==0) q.push(Dout[ft][i]);
		}
	}
	cout<<ans;
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值