12.23~12.24最大食物链计数(拓扑序列的数量),图的遍历(每个点所能到达的最大编号,dp方程的dfs),查找文献(保证字典序的图的dfs,bfs),杂务(拓扑序列中的最长路径)最长路(拓扑变式

P4017 最大食物链计数

题目描述

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

Delia 非常急,所以你只有 11 秒的时间。

由于这个结果可能过大,你只需要输出总数模上 8011200280112002 的结果。

输入格式

第一行,两个正整数 �、�n、m,表示生物种类 �n 和吃与被吃的关系数 �m。

接下来 �m 行,每行两个正整数,表示被吃的生物A和吃A的生物B。

输出格式

一行一个整数,为最大食物链数量模上 8011200280112002 的结果。

分析

就是最弱的到最强的路径数量,每次访问最弱的(入度为0)的点,然后由这个最弱的可以到它所连接的强者,即它所连接的强者多了这个弱者的路径方式,而怎么到弱者,即弱者的路径方式,取决于上层。所以这实际上就是一个递归的过程;

或者可以说是一个dp过程

迭代,找点,处理,

用vector存图,不存权重,只存起点与终点

#include
#include
#include
#include
using namespace std;
//该怎么知道拓扑排序后的序列长度?
#define ll long long
const int N = 5e3 + 2;
const int mod = 80112002;
int n, m;
int in[N], out[N];//出度用以检测是否为拓扑排序的终点
queueq;//存入所有满足条件的点
vectornei[N];//记录以该点为起点的所有终点
int ans;
int num[N];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int x, y;
		cin >> x >> y;
		in[y]++, out[x]++;
		nei[x].push_back(y);
	}
	for (int i = 1; i <= n; i++) {
		if (!in[i]) {
			q.push(i);
			num[i] = 1;
		}
	}
	while (!q.empty()) {
		int tot = q.front();
		q.pop();
		for (int i = 0; i < nei[tot].size(); i++) {
			int next = nei[tot][i];
			in[next]--;
			num[next] += num[tot] % mod;//到达next的路径,需要并上到达tot的路径
			//这里的加法,逻辑上理解就是并
			if (!in[next]) {
				q.push(next);
			}
		}
	}
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		if (!out[i]) {
			ans += num[i] % mod;
		}
	}
	cout << ans;
	return 0;
}

另一种风格 ,邻接矩阵存图

#include
#include
using namespace std;
const int mod = 80112002;
int n, m, ans = 0;
int num[5002], in[5002], out[5002], g[5002][5002];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int a, b;
		cin >> a >> b;
		g[a][b] = 1;
		in[b]++, out[a]++;
	}
	queueq;//队列记录所有满足条件的点,然后依次处理
	for (int i = 1; i <= n; i++) {
		if (!in[i]) {
			q.push(i);
			num[i] = 1;
		}
	}//初始化队列,先找到拓扑的头
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		for (int i = 1; i <= n; i++) {
			if (g[cur][i]) {
				in[i]--;//处理掉这条边,并把起点的路径并入到终点的路径,向上递归
				num[i] += num[cur] % mod;
				if (!in[i]) {
					if (!out[i]) {//说明是拓扑排序的尾端
						ans += num[i] % mod;//做总结
						continue;//不往队列里加这个满足条件的点了,省下一次继续循环找出边
					}
					q.push(i);//不然就说明不是终点,还可以继续找
				}
			}
		}
	}
	cout << ans;
	return 0;
}

注意取模的写法 

需要注意,num[i]+=num[cur]%mod,这个写法是不正确的

运算顺序是从右到左,即先是加数被取模,再是加到被加数上的,这个顺序是错误的,要么先加后取模,要么拆成两行去写

P3916 图的遍历

题目描述

给出 �N 个点,�M 条边的有向图,对于每个点 �v,求 �(�)A(v) 表示从点 �v 出发,能到达的编号最大的点。

输入格式

第 11 行 22 个整数 �,�N,M,表示点数和边数。

接下来 �M 行,每行 22 个整数 ��,��Ui​,Vi​,表示边 (��,��)(Ui​,Vi​)。点用 1,2,…,�1,2,…,N 编号。

输出格式

一行 �N 个整数 �(1),�(2),…,�(�)A(1),A(2),…,A(N)。

分析

类似于递归,就是每个点编号最大能到哪,不仅取决于自己,也取决于它所能到达的点能到的最大编号

这又有点类似于并查集

反向建边+DFS

这样每个点只遍历一次

#include
#include
#include
using namespace std;
#define maxl 100010
int n, m, a[maxl];
vectorg[maxl];
void dfs(int x, int d) {
	if (a[x])return;//如果已经有值了,由于是从大到小的顺序,所以此时的d一定比a[x]小
	a[x] = d;//不然就说明此时x所能到达的最大编号为d
	for (int i = 0; i < g[x].size(); i++) {//遍历的时候顺着来路往回走
		dfs(g[x][i], d);//传入第二个参数为d,即能到达这个点的其他节点,尝试以d来更新
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		g[v].push_back(u);//反向键边,就是说之前存储的是由起点到终点的方式,现在是记录每个终点所对应的起点
	}
	for (int i = n; i; i--) {//从大到小遍历,这样就可以保证访问到小节点时,可以保证最大的编号
		dfs(i, i);//每个节点保底遇到的最大编号为其自身
	}//这一步为精髓
	for (int i = 1; i <= n; i++) {
		cout << a[i] << " ";
	}
	return 0;
}
#include
#include
#include
using namespace std;
#define maxl 100010
int n, m;
int dp[maxl];//表示每个点所能到达的最大编号
vectorg[maxl];//存储能到达这个点所有起点
void dfs(int s, int d) {
	if (dp[s])return;//如果已经被染过,那就不再染了,因为在此被染之前就被染,那么数值一定是更大的
	dp[s] = d;//没被染过,进行更新
	for (int i = 0; i < g[s].size(); i++) {//向四周能达到的点进行染色
		dfs(g[s][i], d);
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		g[v].push_back(u);
	}
	for (int i = n; i >= 1; i--) {
		dfs(i, i);//这个遍历只是单纯的从编号最大的开始遍历,并不一定是有向图的终点
	}//只是从节点最大的点开始蔓延,这样能够保证,由这个点蔓延到的其他所有可达点的dp都是满足要求的最大的
	//就相当于按从大到小的顺序开始滴墨水
	for (int i = 1; i <= n; i++) {
		cout << dp[i] << " ";
	}
	return 0;
}
#include
#include
#include
using namespace std;
#define maxl 100010
struct edge {
	int v, next;
}e[maxl];
int alist[maxl];
int cnt = 0;
void add(int u, int v) {
	e[++cnt].v = v;
	e[cnt].next = alist[u];
	alist[u] = cnt;
}
int n, m;
int res[maxl];
queueq;
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		add(v, u);
	}
	for (int i = n; i >= 1; i--) {
		if (res[i])continue;
		q.push(i);//从这个点开始,队列中的元素都是可以到达这个点的
		//担心的是,在队列当中没有进行res判断,那会不会出现编号大的节点可以到达这个点,然后在这个节点的队列中会把原来的大编号节点的答案覆盖掉
		while (!q.empty()) {
			int cur = q.front();
			q.pop();//队列中的,
			res[cur] = i;
			for (int next = alist[cur]; next; next = e[next].next) {
				if (!res[e[next].v])q.push(e[next].v);//担心是正确的,即只有还没被访问过的,没被染过色的节点才会继续染色,否则不入队列
			}
		}
	}
	for (int i = 1; i <= n; i++) {
		cout << res[i] << " ";
	}
	return 0;
}

P5318 【深基18.例3】查找文献

小 K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个(也有可能没有)参考文献的链接指向别的博客文章。小 K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献(如果他之前已经看过这篇参考文献的话就不用再看它了)。

假设洛谷博客里面一共有 �(�≤105)n(n≤105) 篇文章(编号为 1 到 �n)以及 �(�≤106)m(m≤106) 条参考文献引用关系。目前小 K 已经打开了编号为 1 的一篇文章,请帮助小 K 设计一种方法,使小 K 可以不重复、不遗漏的看完所有他能看到的文章。

这边是已经整理好的参考文献关系图,其中,文献 X → Y 表示文章 X 有参考文献 Y。不保证编号为 1 的文章没有被其他文章引用。

12.23~12.24最大食物链计数(拓扑序列的数量),图的遍历(每个点所能到达的最大编号,dp方程的dfs),查找文献(保证字典序的图的dfs,bfs),杂务(拓扑序列中的最长路径)最长路(拓扑变式_第1张图片

请对这个图分别进行 DFS 和 BFS,并输出遍历结果。如果有很多篇文章可以参阅,请先看编号较小的那篇(因此你可能需要先排序)。

输入格式

共 �+1m+1 行,第 1 行为 2 个数,�n 和 �m,分别表示一共有 �(�≤105)n(n≤105) 篇文章(编号为 1 到 �n)以及�(�≤106)m(m≤106) 条参考文献引用关系。

接下来 �m 行,每行有两个整数 �,�X,Y 表示文章 X 有参考文献 Y。

输出格式

共 2 行。 第一行为 DFS 遍历结果,第二行为 BFS 遍历结果

分析

用链式前向星,再用一个bool数组

dfs中每层都

//还需要保证在遍历的时候为字典序,怎么办?
//dfs怎么为字典序
//bfs可以用字典序,优先队列,
//不行,用优先队列按编号顺序的话,会乱掉,不再是层序
//这个字典序,应该是在可以访问的里面按层序

bool vis[maxn];
//void dfs(int u) {
//	cout << u << " ";
//	for (int i = head[u]; i; i = e[i].next) {
//		dfs(e[i].)
//	}
//}
void bfs() {
	priority_queueq;
	for (int i = 1; i <= n; i++)vis[i] = 0;
	q.push(1);
	vis[i] = 1;
	while (!q.empty()) {
		int cur = q.top();
		q.pop();
		for (int i = head[i]; i; i = e[i].next) {
			if (!vis[e[i].u])q.push(e[i].u);
		}
	}
}

vector存图,节点

用head数组的话,数组的每个坑存的是这个节点的第一个编号

如果用vector,由于需要保证节点编号顺序,则开为vectorg[n],实际为一个二维数组,记录的就直接是每个节点下的边

用int的话,二维数组每个坑只能保存一个信息,即这条边的终点

如果还要保存权重,就用vectorg[n],这样就能访问到每条边的权重

注意vector在用sort排序时,为v.begin(),v.end(),不可类似于数组,直接写个名字加长度

这个题了每条边只有起点与终点的信息,所以直接用,保存终点即可

要进行排序,就是1~n,n个容器数组进行一次排序,代价还是比较高的

set存图

用set主要就是因为它可以保证内部数据有序,这样就省去了排序的步骤

#include
#include
#include
#include
using namespace std;
const int maxn = 1e5 + 2;
//vectorg[maxn];
//int n, m;
//cin >> n >> m;
//for (int i = 1; i <= m; i++) {
//	int u, v;
//	cin >> u >> v;
//	g[u].push_back(v);
//}
//bool cmp()
sete[maxn];
bool vis[maxn] = { 0 };
int n, m;
void init() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		e[u].insert(v);
	}
}
void dfs(int x) {
	if (vis[x])return;//注意记得这一行,如果已经访问过了就退出
	vis[x] = 1;
	cout << x << " ";
	for (auto i : e[x])dfs(i);//遍历数据结构,都可以用auto去进行
}
void bfs() {
	queueq;
	q.push(1);
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		if (vis[cur])continue;
		vis[cur] = 1;
		cout << cur << " ";
		for (auto i : e[cur]) {
			q.push(i);
			//if (!vis[i])q.push(i);//注意不能这么放,会出问题,比如有一个元素是上一层共同的孩子,那么在上一层时,就都会把这个元素入队
			//因为再上一层时这个元素还没访问过,所以这个元素就会被多次入队
			//在树的遍历时不会出现这个问题是因为每个元素只被唯一元素所指定,即入度为1,但这图里不行
			//所以这个bfs,只能无差别入队,然后在出队的时候去检验资格
		}
	}
}
int main() {
	init();
	dfs(1);
	cout << endl;
	for (int i = 1; i <= n; i++)vis[i] = 0;
	bfs();
	return 0;
}

复写版

#include
#include
#include
#include
using namespace std;
const int maxn = 1e5 + 2;
int n, m;
sete[maxn];
bool vis[maxn];
void dfs(int x) {
	if (vis[x])return;
	vis[x] = 1;
	cout << x << " ";
	for (auto i : e[x])dfs(i);
}
void bfs() {
	queueq;
	q.push(1);
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		if (vis[cur])continue;
		vis[cur] = 1;
		cout << cur << " ";
		for (auto i : e[cur])q.push(i);
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int u, v;
		cin >> u >> v;
		e[u].insert(v);
	}
	dfs(1);
	cout << endl;
	for (int i = 1; i <= n; i++)vis[i] = 0;
	bfs();
	return 0;
}

P1113 杂务

John 有需要完成的 �n 个杂务的清单,并且这份清单是有一定顺序的,杂务 � (�>1)k (k>1) 的准备工作只可能在杂务 11 至 �−1k−1 中。

写一个程序依次读入每个杂务的工作说明。计算出所有杂务都被完成的最短时间。当然互相没有关系的杂务可以同时工作,并且,你可以假定 John 的农场有足够多的工人来同时完成任意多项任务。

输入格式

第1行:一个整数 � (3≤�≤10,000)n (3≤n≤10,000),必须完成的杂务的数目;

第 22 至 �+1n+1 行,每行有一些用空格隔开的整数,分别表示:

  • 工作序号(保证在输入文件中是从 11 到 �n 有序递增的);

  • 完成工作所需要的时间 ��� (1≤���≤100)len (1≤len≤100);

  • 一些必须完成的准备工作,总数不超过 100100 个,由一个数字 00 结束。有些杂务没有需要准备的工作只描述一个单独的 00。

保证整个输入文件中不会出现多余的空格。

输出格式

一个整数,表示完成所有杂务所需的最短时间。

分析

就是拓扑排序,然后每个节点都有一个时间(不是边),然后最后问完成任务的最短时间,实际上就是所有拓扑序列中的最长时间

由食物链,可以根据dp+=dp,确定拓扑数量,用出度为0,检测拓扑的终点,入度为0检测拓扑的起点

问最长时间,实际上就是出度为0的所有点的最大值 

自己写的版

每个任务开始的时间,就是前置任务所需时间里的最大值,然后最早结束时间就是最早开始时间加上自己的完成所需时间

#include
#include
#include
#include
using namespace std;
const int maxn = 1e4 + 2;
int in[maxn], out[maxn];
int n, w[maxn], index;//w数组后续被更新为各个任务的最早结束时间
vectorg[maxn];
int pre[maxn];//记录前驱工作的最大工作时间,即为各个任务的最早开始时间
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> index >> w[i];
		int t;
		while (cin >> t) {
			if (!t)break;
			g[t].push_back(i);
			in[i]++, out[t]++;
		}
	}
	queueq;
	for (int i = 1; i <= n; i++) {
		if (!in[i]) { 
			q.push(i);
		}
	}
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		for (int i = 0; i < g[cur].size(); i++) {
			in[g[cur][i]]--;
			pre[g[cur][i]] = max(pre[g[cur][i]], w[cur]);
			if (!in[g[cur][i]]) {
				w[g[cur][i]] += pre[g[cur][i]];
				q.push(g[cur][i]);
			}
		}
	}
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		if (!out[i]) {
			ans = max(ans, w[i]);
		}
	}
	cout << ans;
	return 0;
}

精简版

#include
#include
#include
#include
using namespace std;
const int maxn = 1e5 + 2;
int n, maxans = 0;
int ans[maxn];
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) {
		int w, t, pre = 0;
		cin >> i >> w;
		while (cin >> t && t) {
			pre = max(ans[t], pre);
		}
		ans[i] = pre + w;
		maxans = max(maxans, ans[i]);
	}
	cout << maxans;
	return 0;
}

P1807 最长路

设 �G 为有 �n 个顶点的带权有向无环图,�G 中各顶点的编号为 11 到 �n,请设计算法,计算图 �G 中 1,�1,n 间的最长路径。

输入格式

输入的第一行有两个整数,分别代表图的点数 �n 和边数 �m。

第 22 到第 (�+1)(m+1) 行,每行 33 个整数 �,�,�u,v,w(�<�u

输出格式

输出一行一个整数,代表 11 到 �n 的最长路。

若 11 无法到达 �n,请输出 −1−1。

分析

就是问到指定终点的最长路径

拓扑实际上就是一个不断收缩的过程,就是递归,dp的过程

而dp方程就体现在收缩时所处理、保存的信息;收缩的过程就是划分子任务,减少问题规模的过程

在这里每收缩掉入度为0的点,就保存最长的路径长度,然后让可达的点,都在这个基础上去加上由出发点到它的必要的路径长度,就是每个点所记录的最大路径长度,每个点可能有多条路径可以到达,那么保留的就是最大的那个路径长度的路径

自己写的版(误)

12.23~12.24最大食物链计数(拓扑序列的数量),图的遍历(每个点所能到达的最大编号,dp方程的dfs),查找文献(保证字典序的图的dfs,bfs),杂务(拓扑序列中的最长路径)最长路(拓扑变式_第2张图片

原因是因为,拓扑排序是从入度为0的点开始的,这也就导致如果能到n,那么不一定是从以1为起点的路线到达的n,而可能是其他入度为0的点所达到的

#include
#include
#include
#include
using namespace std;
const int maxn = 1502;
const int maxm = 5e4 + 2;
int n, m, cnt = 0;
int dp[maxn];//记录为到达这个点的最大路径长度
int in[maxn];
struct edge {
	int u, w, next;
}e[maxm];
int head[maxn];
void add(int u, int v, int w) {
	e[++cnt].u = v;
	e[cnt].w = w;
	e[cnt].next = head[u];
	head[u] = cnt;
}
int main() {
	cin >> n >> m;
	//n不一定是终点
	//拓扑
	for (int i = 1; i <= m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w);
		in[v]++;
	}
	queueq;
	for (int i = 1; i <= n; i++) {
		if (!in[i])q.push(i);
	}
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		for (int i = head[cur]; i; i = e[i].next) {
			int v = e[i].u;
			dp[v] = max(dp[v], dp[cur] + e[i].w);
			in[v]--;
			if (!in[v]) {
				q.push(v);
			}
		}
	}
	cout << dp[n];
	return 0;
}

改良版 

实际上拓扑排序的过程就是bfs的过程,就是不断取出队列的点,然后通过它去影响周围联通的点,再把符合要求的加入到队列中来去进行染色

但是如果直接用BFS也会有问题,因为只是通过一条路径就遍历到某个点的话,就把这个点的vis标志为1,那么就无法通过其他路径去访问到这个点,自然也就无法判断达到这个点的路径长度最大值,所以还得是结合入度为0的判断,只不过一开始的时候不是把所有入度为0的点加入,而是只加入1

但这样还是不对,因为加入初始时加入1,就意味着是从1开始,但是N可能有不是1的路径的起点,也就是说只从1开始,可能不能遍历完所有能到n的路径,也就无法让n加入队列,不仅如此,从1到n的路径上的点,的入度,可能也有这些孤点的路径,而这,也就会导致从1到n路径上的点无法被加入到队列当中,自然也就导致从1到n的某些路径失去,因为入队的条件是入度为0,而不是被访问到了(通过vis),智能一点的话应该是从1到n的路径之间的点的入度为0

 12.23~12.24最大食物链计数(拓扑序列的数量),图的遍历(每个点所能到达的最大编号,dp方程的dfs),查找文献(保证字典序的图的dfs,bfs),杂务(拓扑序列中的最长路径)最长路(拓扑变式_第3张图片

处理方法就是说从入度为0的,编号不是1的点开始做处理,使那些点可达的点的入度都减去1,注意是可直接到达的点,即一步可达的;而如果处理后又产生了入度为0的点,就继续操作,直到不再发生这种情况(bfs的过程)。

实际上就是在执行类似于删点的操作

还需要注意 ,由于存在负权边,所以在初始化时不能直接设置为0,而是要设为负无穷,不然0就有可能直接为最长路长

 

#include
#include
#include
#include
using namespace std;
const int maxn = 1502;
const int maxm = 5e4 + 2;
int n, m, cnt = 0;
int dp[maxn];//记录为到达这个点的最大路径长度
int in[maxn];
struct edge {
	int u, w, next;
}e[maxm];
int head[maxn];
void add(int u, int v, int w) {
	e[++cnt].u = v;
	e[cnt].w = w;
	e[cnt].next = head[u];
	head[u] = cnt;
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v, w);
		in[v]++;
	}
	queueq;
	for (int i = 2; i <= n; i++) {
		dp[i] = -1e9;//可以保证起点1的dp为0
		if (!in[i]) {
			q.push(i);
		}
	}//初始化1后面所有点的dp,并把那些入度为0的非1点做处理
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		for (int i = head[cur]; i; i = e[i].next) {
			in[e[i].u]--;//类似于删点
			if (!in[e[i].u])q.push(e[i].u);
		}
	}
	//经此处理掉其他入度为0的非1点
	q.push(1);
	while (!q.empty()) {
		int cur = q.front();
		q.pop();
		for (int i = head[cur]; i; i = e[i].next) {
			int v = e[i].u;
			dp[v] = max(dp[v], dp[cur] + e[i].w);
			in[v]--;
			if (!in[v]) {
				q.push(v);
			}
		}
	}
	if (dp[n]<0)cout << -1;
	else cout << dp[n];
	return 0;
}

其他算法待改良版

你可能感兴趣的:(算法,算法,c++,数据结构)