拓扑排序也算是图论的一部分,主要是解决有向无环图的问题。
在算阶上的板子如下
void add(int x,int y){v[++tot]=y;nxt[tot]=head[x];head[x]=tot;deg[y]++;}
void topsort()
{
queue<int> q;
for(int i=1;i<=n;i++)if(deg[i]==0){q.push(i);}
while(q.size())
{
int x=q.front();q.pop();
for(int i=head[x];i;i=nxt[i])
{
int y=v[i];
if(--deg[y]==0) q.push(y);
}
}
}
(码风丑陋是因为我压行了)
void add(int x,int y){v[++tot]=y;nxt[tot]=head[x];head[x]=tot;deg[y]++;}
这一部分不用说,是在邻接表中添加一个有向边。
像这样一个图,我们首先让入度为
0
0
0的
1
,
3
,
6
,
7
1,3,6,7
1,3,6,7入队
然后将
1
1
1中的东西上传到
2
2
2上,
3
3
3上的东西上传到
2
2
2上,此时
2
2
2的入度为
0
0
0,所以也进入队列里,而
1
1
1的出度为
0
0
0,所以出队,队列变为
3
,
6
,
7
,
2
3 ,6,7,2
3,6,7,2
然后将
3
3
3上传到
4
4
4,
4
4
4的入度减减,
3
3
3的出度为
0
0
0,出队
以此类推,直到队列里面什么也没有
直接上一道例题吧
题目描述
小W在上数理逻辑的课。他想要自创一套公理系统。
现在小W有 n n n 个命题,标号为 1 − n 1-n 1−n,他有 m m m 个推出关系。每个推出关系都是形如 P ⇒ Q P \Rightarrow Q P⇒Q,意思是如果 P P P 被证明是对的,那么就可以推出 Q Q Q 是对的。你可以把这些推出关系想成一个有向图。他发现这张图没有环。他现在想要从中选出一些命题设为公理,要求其它所有命题都可以通过这些公理推出。他想要最小化选出的公理数,并输出任意一种方案即可。
小W给出公理之后,小W的朋友小Y想要通过这些公理去证明所有命题。对于每一个命题 T T T,他想要用最少的推导次数证明这个命题。即找一条路径 v 1 , v 2 , … , v k = T v_1,v_2,\dots,v_k=T v1,v2,…,vk=T,满足 v 1 v_1 v1 是一条公理,并且 ∀ i ∈ [ 1 , k − 1 ] , v i ⇒ v i + 1 \forall\ i \in [1,k-1],\ v_i \Rightarrow v_{i+1} ∀ i∈[1,k−1], vi⇒vi+1,则推导次数是 k − 1 k-1 k−1。
输入格式
第一行两个整数
n
,
m
n, m
n,m。
接下来
m
m
m 行,每行两个整数
P
,
Q
P,Q
P,Q,表示第
P
P
P 个命题可以推出第
Q
Q
Q 个命题。
输出格式
第一行一个整数
k
k
k,表示公理的个数。
第二行
k
k
k 个整数,表示选出的公理,按从小到大输出。
第三行
n
n
n 个整数,第
i
i
i 个整数表示第
i
i
i 个命题的最少推导次数。
样例数据
input
7 6
1 3
2 3
3 4
3 5
4 6
5 6
output
3
1 2 7
0 0 1 2 2 3 0
数据规模与约定
对于
20
%
20\%
20% 的数据,
n
,
m
≤
15
n, m \le 15
n,m≤15。
对于
60
%
60\%
60% 的数据,
n
,
m
≤
1000
n, m \le 1000
n,m≤1000。
对于
100
%
100\%
100% 的数据,
1
≤
n
,
m
≤
5
×
1
0
5
1 \le n, m \le 5 \times10^5
1≤n,m≤5×105。
时间限制: 2 s 2 \text {s} 2s
空间限制: 512 MB 512 \text {MB} 512MB
解:
前两个任务就直接标记一下,哪些是可以被推导的,如果不能推导则为公理,及其简单
而第三行的输出虽然也可搜索+剪枝过去,但是这里我用的是拓扑
当然,前两个任务也可以在拓扑排序时解决
由样例可以得到这样一个有向无环图
1
,
2
1,2
1,2和
7
7
7入度为
0
0
0
所以这三个不能被推导的是公理
套板子是不能直接A掉的
在这里我们先让
1
,
2
,
7
1,2,7
1,2,7入队,然后由于1和2都可以影响到
3
,
3,
3,所以上传
a[y]=min(a[y],a[x]+1);//当前是a[y],由a[x]上传,所以是+1
取个最小值
再有一点不同的就是我们要给所有点赋个比较大的初始值,不然不会受到影响被修改
然后给入度为
0
0
0的几个点赋值为
0
0
0,因为它们是公理,不用推导
配上极其丑陋的代码如下:
#include<bits/stdc++.h>//万能头文件
using namespace std;//虽然不知道有什么用,但是每次都写
#define N 500010//数组大小
int nxt[N],a[N],v[N],head[N],deg[N],n,m,p,q,k,cnt,tot;bool flag[N];//声明各种变量
void add(int x,int y){v[++tot]=y;nxt[tot]=head[x];head[x]=tot;deg[y]++;}//建图,deg为入度
void topsort()//起个函数名
{//topsort的前大括号
queue<int> q;//队列
for(int i=1;i<=n;i++)if(deg[i]==0){q.push(i);a[i]=0;}//入度为0的进队且需要0次推导
while(q.size())//当队列中还有元素
{//while循环的前大括号
int x=q.front();q.pop();//使x为队首,并且将队首出队
for(int i=head[x];i;i=nxt[i])//让i一直向上爬,知道没有“上一个边”
{//for循环的前大括号
int y=v[i];//用y代替写v[i],图个方便
a[y]=min(a[y],a[x]+1);//当前点的最少推导次数,可能被多次更新,取min即可
if(--deg[y]==0) q.push(y);//该点的入度减一0,若为0则进队
}//for循环的后大括号
}//while循环的后大括号
}//topsort的后大括号
int main()//主函数
{//主函数的前大括号
scanf("%d%d",&n,&m);k=n;//输入有n个命题,m个推导关系
for(int i=1;i<=m;i++)//m个推导
{//for循环的前大括号
scanf("%d%d",&p,&q);add(p,q);//输入p,q表示p能推导q,连一个p到q的有向线段
if(flag[q]==0){flag[q]=1;k--;}//看是否能推导,有几个能被推导
}printf("%d\n",k);//输出至少有几个公理
for(int i=1;i<=n;i++) a[i]=99999999;topsort();//给每个点赋初值
for(int i=1;i<=n;i++){if(flag[i]==0){printf("%d ",i);}}puts("");//输出哪些是公理
for(int i=1;i<=n;i++)printf("%d ",a[i]);//输出至少需要多少次推导,及拓扑序
return 0;//结束
}//主函数的后大括号
然后就能愉快地 A A A掉了
再看一个水题
题目描述
M r . Z Mr.Z Mr.Z决定给每位员工发奖金。
公司决定以每个人本年在公司的贡献为标准来计算他们得到奖金的多少。于是Mr.Z下令召开m方会谈。每位参加会谈的代表提出了自己的意见:“我认为员工 a a a的奖金应该比 b b b高!”
M r . Z Mr.Z Mr.Z决定要找出一种奖金方案,满足各位代表的意见,且同时使得总奖金数最少。每位员工奖金最少为 100 100 100元。
所有数都是整数
输入格式
第一行两个整数 n , m n,m n,m,表示员工总数和代表数;
以下 m m m行,每行 2 2 2个整数 a , b , a,b, a,b,表示某个代表认为第a号员工奖金应该比第b号员工高。
输出格式
若无法找到合理方案,则输出 “ P o o r X e d ” “Poor Xed” “PoorXed”;否则输出一个数表示最少总奖金。
样例数据
input
2 1
1 2
output
201
数据规模与约定
80 % 80\% 80%的数据满足 n < = 1000 , m < = 2000 n<=1000,m<=2000 n<=1000,m<=2000;
100 % 100\% 100%的数据满足 n < = 10000 , m < = 20000 n<=10000,m<=20000 n<=10000,m<=20000。
时间限制: 1 s 1 \text {s} 1s
空间限制: 256 MB 256 \text {MB} 256MB
解:
其实还是拓扑的板子,然而不过是把刚刚那个题的箭头反过来指,然后把取
m
i
n
min
min换成取
m
a
x
max
max,初值定成
0
0
0
但是这个题有一点说是如果成环的话输出
“
P
o
o
r
X
e
d
”
“Poor Xed”
“PoorXed”
然后就是如何判环
其实正常每个数都应该进队一次,而如果有环的话,环中的数是不会进队的
所以这时候加个计数器即可
A
C
AC
AC代码
#include<bits/stdc++.h>
using namespace std;
#define N 200000
int v[N],nxt[N],jishu,head[N],deg[N],n,m,ans,a[N],p,q,tot;
void add(int x,int y){v[++tot]=y;nxt[tot]=head[x];head[x]=tot;deg[y]++;}
void topsort()
{
queue<int> q;
for(int i=1;i<=n;i++)if(deg[i]==0){q.push(i);a[i]=0;}
while(q.size())
{
jishu++;//q中每有一个都会++
int x=q.front();q.pop();
for(int i=head[x];i;i=nxt[i])
{
int y=v[i];
a[y]=max(a[y],a[x]+1);//这里取最大不解释
if(--deg[y]==0) q.push(y);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){scanf("%d%d",&p,&q);add(q,p);}//反过来连,因为是q应该比p多至少1RMB
topsort();if(jishu<n){puts("Poor Xed");return 0;}
for(int i=1;i<=n;i++){ans+=(100+a[i]);}//因为每人至少100RMB
cout<<ans;
}
第三个水题
题目描述
设 G G G为有 n n n个顶点的有向无环图, G G G中各顶点的编号为 1 1 1到 n n n,且当为G中的一条边时有 i < j i < j i<j。
设 w ( i , j ) w(i,j) w(i,j)为边的长度,请设计算法,计算图 G G G中 < 1 , n > <1,n> <1,n>间的最长路径。
输入格式
第一行有两个整数 n n n和 m m m,表示有 n n n个顶点和m条边,
接下来m行中每行输入 3 3 3个整数 a , b , v a,b,v a,b,v(表示从 a a a点到 b b b点有条边,边的长度为v)。
输出格式
一个整数,即 1 1 1到 n n n之间的最长路径.如果 1 1 1到 n n n之间没连通,输出 − 1 -1 −1。
样例数据
input
2 1
1 2 1
output
1
数据规模与约定
20 % 20\% 20%的数据, n ≤ 100 , m ≤ 1000 n≤100,m≤1000 n≤100,m≤1000
40 % 40\% 40%的数据, n ≤ 1 , 000 , m ≤ 10000 n≤1,000,m≤10000 n≤1,000,m≤10000
100 % 100\% 100%的数据, n ≤ 1 , 500 , m ≤ 50000 n≤1,500,m≤50000 n≤1,500,m≤50000,最长路径不大于 1 0 9 10^9 109
时间限制: 1 s 1 \text {s} 1s
空间限制: 256 MB 256 \text {MB} 256MB
解:
这次我是设的结构体,建图时可能更直观一点吧,顺便在这里再写写是如何建的图
void lian(int x,int y,int z)//这个边是从x指向y,且长度为z
{
rudu[y]++;//指向点y的边多一个,y的入度++
e[++tot].y=y;//tot是这个边的编号,y是该边指向y
e[tot].w=z;//该边的长度w=z
e[tot].shang=hd[x];//记录从x点出发的上一个边的下标
hd[x]=tot;//更新最新的从x点出发的点的下标
}
写这个题的时候我算是才真正明白建图的每一步是做的什么,自己写的时候也不用看着板子敲了,所以也用了更直观的变量名。
做一个指针指向从该点出发的上一个边是为了达到这种效果
直接把这个点所有要传递的信息上传。
好了回归这个题
一开始想的是每次
v[y]=max(v[y],v[x]+e[i].w);
取
y
y
y点已有的消耗与
x
x
x点的消耗和这个边的最大值
但是并不能过,因为不一定是从
1
1
1点出发
然后我们可以把所有点的初始值设为一个极小的负数(这个数的绝对值一定要大于最长的长度),然后
1
1
1的初始值设为
0
0
0
这是很显然的,最终结果能大于
0
0
0的一定被
1
1
1直接或间接更新过
而结果小于
0
0
0的话一定不能到达
#include<bits/stdc++.h>
using namespace std;
#define N 50010
#define int long long
int a,b,c,m,n,tot,v[N],rudu[N],hd[N];struct{int y,w,shang;}e[N];
void lian(int x,int y,int z)
{
rudu[y]++;
e[++tot].y=y;
e[tot].w=z;
e[tot].shang=hd[x];
hd[x]=tot;
}
void tuopu()
{
queue<int> q;
for(int i=1;i<=n;i++){v[i]=-9999999999999;if(rudu[i]==0)q.push(i);} v[1]=0;
while(q.size())
{
int x=q.front();q.pop();
for(int i=hd[x];i;i=e[i].shang)
{
int y=e[i].y;
v[y]=max(v[y],v[x]+e[i].w);
if(--rudu[y]==0) q.push(y);
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++) {scanf("%lld%lld%lld",&a,&b,&c);lian(a,b,c);}
tuopu();if(v[n]<0)puts("-1");else cout<<v[n];
return 0;
}
水题×4
题目描述
F i r e D a n c e r FireDancer FireDancer来到一个神秘岛,他要从岛的西头到东头然后在东头的码头离开。可是当他走了一次后,发现这个岛的景色非常的美丽,于是他从东头的传送门传到了西头,换了一种走法又走了一遍。发现还不过瘾,又走了一遍……终于, F i r e D a n c e r FireDancer FireDancer把所有的从西头到东头的路径都走了一遍。他站在岛东头的海滩上,突然想到了一个问题,那就是他一共花了多少时间。他把这个问题交给了你。
F i r e D a n c e r FireDancer FireDancer把这个岛抽象成了一个图,共 n n n个点代表路的相交处, m m m条边表示路,边是有向的(只能按照边的方向行走),且可能有连接相同两点的边。输入保证这个图没有环,而且从西头到东头至少存在一条路径。两条路径被认为是不同的当且仅当它们所经过的路不完全相同。
输入格式
第一行为 5 5 5个整数, n 、 m 、 s 、 t 、 t 0 n、m、s、t、t0 n、m、s、t、t0,分别表示点数(编号是从 1 1 1到 n n n),边数,岛西头的编号,岛东头的编号和传送一次的时间。
以后m行,每行 3 3 3个整数, x 、 y 、 t x、y、t x、y、t,表示从点 x x x到点 y y y有一条行走耗时为t的路。 且: 2 < = n < = 10000 2<=n<=10000 2<=n<=10000; 1 < = m < = 50000 ; t < = 10000 ; t 0 < = 10000 1<=m<=50000;t<=10000;t0<=10000 1<=m<=50000;t<=10000;t0<=10000
输出格式
若总耗时为 t o t a l total total,则输出 t o t a l m o d 10000 total\;\; mod \;\; 10000 totalmod10000( t o t a l total total对 10000 10000 10000取余)。
样例数据
input
3 4 1 3 7
1 2 5
2 3 7
2 3 10
1 3 15
output
56
[样例说明]
共有3条路径可以从点
1
1
1到点
3
3
3,分别是
1
−
2
−
3
,
1
−
2
−
3
,
1
−
3
1-2-3,1-2-3,1-3
1−2−3,1−2−3,1−3。时间计算为:
(
5
+
7
)
+
7
+
(
5
+
10
)
+
7
+
(
15
)
=
56
(5+7)+7 +(5+10)+7 +(15)=56
(5+7)+7+(5+10)+7+(15)=56
数据规模与约定
时间限制: 1 s 1 \text {s} 1s
空间限制: 256 MB 256 \text {MB} 256MB
解:
我们很自然可以想到跑两遍拓扑,第一遍找 s s s到 t t t所能经过的点,然后把无用的点除掉,第二遍拓扑的时候一开始只让 s s s点进队。
#include<bits/stdc++.h>
using namespace std;
#define N 500010
#define int long long
struct bian{int x,y,w,shang;}e[N];
bool flag[N];
int rudu[N],rudu1[N],hh[N],hd[N],n,m,s,tot,v[N],t,cs,ans,a,b,c;
void lian(int x,int y,int z)
{
rudu[y]++;
rudu1[y]=rudu[y];//给入度备份
e[++tot].y=y;
e[tot].x=x;//这里额外记录了一下每个编的起始点
e[tot].shang=hd[x];
e[tot].w=z;
hd[x]=tot;
}
void tuopu()
{
queue<int> q;hh[s]=1;
for(int i=1;i<=n;i++) {if(rudu[i]==0) q.push(i);}
while(q.size())
{
int x=q.front();q.pop();
for(int i=hd[x];i;i=e[i].shang)
{
int y=e[i].y;
hh[y]=(hh[y]+hh[x])%10000;//hh值不为0的点一定是被s点传递过去的
if(hh[y]>0||hh[x]>0)flag[y]=flag[x]=1;
if(--rudu1[y]==0) q.push(y);
}
}
for(int i=1;i<=m;i++) if(flag[e[i].x]==0)rudu[e[i].y]--;
q.push(s);
while(q.size())
{
int x=q.front();q.pop();
for(int i=hd[x];i;i=e[i].shang)
{
int y=e[i].y;
v[y]=((v[x]+v[y])%10000+((hh[x])*e[i].w)%10000)%10000;
//到x点的所有路径总长度=到y点的所有路径总长度+之前到x点所有路径总长度+到该点的方案数*这个边的长度
if(--rudu[y]==0) q.push(y);
}
}
}
signed main()
{
scanf("%lld%lld%lld%lld%lld",&n,&m,&s,&t,&cs);
for(int i=1;i<=m;i++)
{
scanf("%lld%lld%lld",&a,&b,&c);
lian(a,b,c);
}tuopu();
ans=((v[t])%10000+(((hh[t]-1)%10000)*cs)%10000)%10000;
cout<<ans;
}
我用 h h hh hh记录从 s s s点到该点的方案数,一边记录一边 m o d mod mod,但是这个题的模数极其鬼畜,所以如果某个点的 h h hh hh是 10000 10000 10000的倍数的话,模完直接变成 0 0 0,就凉凉了,所以就又标记了个 f l a g flag flag,然后就 A A A掉了。
再往后的话其实可以写一道特别水的紫题了
落谷题面传送门https://www.luogu.com.cn/problem/P2462
题解传送门[SDOI2007]游戏