图论初学
一.割点
割点就是删掉一些点后,图会分为几个连通块.任意子连通块依然连通
通过两个数组实现:
1. dfn[u]为节点u在搜索树中的次序号
2. low[u]为u或u的子孙中能通过非父亲边追溯到的dfn最小的节点
如果u是根节点,且u有多于一棵子树,则u是割点
如果u不是根节点,且存在u的孩子v,使得dfn(u)<=low(v),也就是说去掉u后,并不能到达u的祖先,则u是割点.
#include<bits/stdc++.h>
using namespace std;
#define MAX 120000
inline int read(){
int x=0,t=1;char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return x*t;
}
struct Line{
int v,next;
}e[MAX<<1];
int n,dfn[MAX],low[MAX];
bool cut[MAX];
int root,m;
int h[MAX],cnt=1,son;
inline void Add(int u,int v){
e[cnt]=(Line){v,h[u]};
h[u]=cnt++;
}
void Tarjan(int u,int ff){
dfn[u]=low[u]=cnt++;
for(int i=h[u];i;i=e[i].next){
int v=e[i].v;
if(!dfn[v]){
Tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
if(u!=root)cut[u]=true;
else son++;
}
}
else
if(v!=ff)
low[u]=min(low[u],dfn[v]);
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=m;++i){
int u=read(),v=read();
Add(u,v);Add(v,u);
}
for(int i=1;i<=n;++i)
if(!dfn[i]){
Tarjan(root=i,son=0);
if(son>=2)cut[root]=true;
}
int tot=0;
for(int i=1;i<=n;++i)
if(cut[i])tot++;
printf("%d\n",tot);
for(int i=1;i<=n;++i)
if(cut[i])
printf("%d ",i);
puts("");
return 0;
}
2.矿场维修
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
Solution
这个题还是比较诡的
先用一个Tarjan把割点全都跑出来
再用dfs去搜索连通块,这个是我不会的,看了yyb大佬的博客,才知道怎么处理
这里的dfs相当于一个染色然后判断的dfs,枚举搜索(要先判断不是割点且不是已经被弄到块里的点).
if(cut[v]&&vis[v]!=Group){
Cut++;
vis[v]=Group;
}
如果当前到达的点不是割点,且跟原点不再一个块中,那就意味着我们搜到了一个新的块,所以这个割点就连接着我和另外一个块,那么这里就CUT++
if(!vis[v])
DFS(v);
如果当前点第一次搜索这个点就把它染成一个颜色
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
#define MAX 501
int Dfn[MAX],vis[MAX],Low[MAX];
bool cut[MAX];
long long Num,Cut,Time,root,rs,m,n,Ans1,Ans2,Case,Group;
void Init();
struct Node{
int v,next;
}e[MAX*MAX];
int h[MAX],cnt;
void Add(int u,int v){
e[cnt]=(Node){v,h[u]};
h[u]=cnt++;
}
void Tarjan(int u,int f){
int v;
Dfn[u]=Low[u]=++Time;
for(int i=h[u];i!=-1;i=e[i].next){
v=e[i].v;
if(!Dfn[v]){
Tarjan(v,u);
Low[u]=min(Low[u],Low[v]);
if(Low[v]>=Dfn[u]){
if(u!=root)
cut[u]=true;
else
rs++;
}
}
else
if(v!=f)
Low[u]=min(Low[u],Dfn[v]);
}
}
void DFS(int u){
int v;
vis[u]=Group;
Num++;
for(int i=h[u];i!=-1;i=e[i].next){
v=e[i].v;
if(cut[v]&&vis[v]!=Group){
Cut++;
vis[v]=Group;
}
if(!vis[v])
DFS(v);
}
}
int main()
{
long long u,v;
Case=1;
while(cin>>m&&m){
Init();
for(int i=1;i<=m;++i){
cin>>u>>v;
Add(u,v);
Add(v,u);
n=max(n,v);
n=max(n,u);
}
for(int i=1;i<=n;++i){
if(!Dfn[i]){
root=i;
rs=0;
Tarjan(i,i);
if(rs>=2)
cut[i]=true;
}
}
for(int i=1;i<=n;++i){
if(!vis[i]&&!cut[i]){
++Group;
Num=Cut=0;
DFS(i);
if(Cut==0){
Ans1+=2;
Ans2*=(Num-1)*Num/2;
}
if(Cut==1){
Ans1+=1;
Ans2*=Num;
}
if(Cut>=2){
;
}
}
}
cout<<"Case "<<Case++<<": "<<Ans1<<" "<<Ans2<<endl;
}
return 0;
}
void Init()
{
memset(h,-1,sizeof(h));
memset(Dfn,0,sizeof(Dfn));
memset(Low,0,sizeof(Low));
memset(cut,0,sizeof(cut));
memset(vis,0,sizeof(vis));
Time=cnt=n=Ans1=Group=0;
Ans2=1;
}
二.拓扑排序
1.在有向图中选一个没有前驱的顶点并且输出
2.从图中删除该顶点和所有以它为尾的弧(白话就是:删除所有和它有关的边)
3.重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环
#include<bits/stdc++.h>
using namespace std;
const int N=1502,INF=1e9;
int n,m,a,b,w,cnt;
int last[N],d[N],f[N];
struct Edge{
int to,next,w;
}edge[50002];
void insert(int u,int v,int w){
edge[++cnt]=(Edge){v,last[u],w};last[u]=cnt;
d[v]++;
}
void Toposort(){
int top=-1,u;
for(int i=2;i<=n;i++)if(!d[i]){d[i]=top;top=i;}
while(top!=-1){
u=top;
top=d[top];
for(int i=last[u];i;i=edge[i].next){
int v=edge[i].to;
edge[i].w=INF;
d[v]--;
if(!d[v]){d[v]=top;top=v;}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d",&a,&b,&w);
insert(a,b,w);
}
Toposort();
for(int i=1;i<=n;i++){
for(int j=last[i];j;j=edge[j].next){
if(edge[j].w!=INF){
int v=edge[j].to;
f[v]=max(f[v],f[i]+edge[j].w);
}
}
}
if(!f[n]){printf("-1\n");return 0;}
printf("%d\n",f[n]);
return 0;
}
三.SPFA
这里贴一个SPFA
#include<bits/stdc++.h>
#define INF 2147483647
#define N 10005
using namespace std;
inline int gi(){
int x=0,k=1; char c=getchar();
while(c<'0'||c>'9'){if(c=='-')k=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*k;
}
int n,m,s,tot=0,dis[N],head[N*50];
bool vis[N];
struct Line{
int next,to,w;
}h[N*50];
void Add(int u,int v,int w){
h[++tot].next=head[u];
h[tot].to=v;
h[tot].w=w;
head[u]=tot;
}
queue<int> q;
inline void spfa(){
for(int i=1; i<=n; i++)
dis[i]=INF;
int u,v;
q.push(s);
dis[s]=0;
vis[s]=1;
while(!q.empty()){
u=q.front(),v;
q.pop();
vis[u]=0;
for(int i=head[u];i;i=h[i].next){
v=h[i].to;
if(dis[v]>dis[u]+h[i].w){
dis[v]=dis[u]+h[i].w;
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
}
int main(){
n=gi(),m=gi(),s=gi();
for(int i=1,u,v,w;i<=m;i++){
u=gi(),v=gi(),w=gi();
Add(u,v,w);
}
spfa();
for(int i=1;i<=n;i++)
cout<<dis[i]<<" ";
return 0;
}
2.job
奶牛们正在找工作。农场主约翰知道后,鼓励奶牛们四处碰碰运气。而且他还加了一条要求:一头牛在一个城市最多只能赚D(1≤D≤1000)美元,然后它必须到另一座城市工作。当然,它可以在别处工作一阵子后又回到原来的城市再最多赚D美元。而且这样的往返次数没有限制。
城市间有P(1≤P≤150)条单向路径连接,共有C(2≤C≤220)座城市,编号从1到C。奶牛贝茜当前处在城市S(1≤S≤C)。路径i从城市A_i到城市B_i(1≤A_i≤C,1≤B_i≤C),在路径上行走不用任何花费。
为了帮助贝茜,约翰让它使用他的私人飞机服务。这项服务有F条(1≤F≤350)单向航线,每条航线是从城市J_i飞到另一座城市K_i(1≤J_i≤C,1≤K_i≤C),费用是T_i(1≤T_i≤50000)美元。如果贝茜手中没有现钱,可以用以后赚的钱来付机票钱。
贝茜可以选择在任何时候,在任何城市退休。如果在工作时间上不做限制,贝茜总共可以赚多少钱呢?如果赚的钱也不会出现限制,就输出-1。
输入输出格式
输入格式:
第一行:5个用空格分开的整数D,P,C,F,S。
第2到第P+1行:第i+1行包含2个用空格分开的整数,表示一条从城市A_i到城市B_i的单向路径。
接下来F行,每行3个用空格分开的整数,表示一条从城市J_i到城市K_i的单向航线,费用是T_i。
输出格式:
一个整数,在上述规则下最多可以赚到的钱数。
Solution
显然是道最短路的题,其实算法标签也告诉你拉.
首先向若工资不超过限制,肯定是重复走到了任意一个城市
所以记录一个所走城市个数,如果大于总城市数c肯定是无限制
如果没办法成环,我们就可以枚举dis[i]记录的到达每个城市时的花费
取个最大值.
#include<bits/stdc++.h>
#define INF 2147483647
#define N 10005
using namespace std;
inline int gi(){
int x=0,k=1; char c=getchar();
while(c<'0'||c>'9'){if(c=='-')k=-1;c=getchar();}
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*k;
}
int Ans=-99999;
int d,p,c,f,s,tot=0,dis[N],head[N*50],du[N];
bool vis[N];
struct Line{
int next,to,w;
}h[N*50];
void Add(int u,int v,int w){
h[++tot].next=head[u];
h[tot].to=v;
h[tot].w=w;
head[u]=tot;
}
queue<int> q;
inline void spfa(){
memset(vis,false,sizeof(vis));
memset(dis,-0x7f7f7f,sizeof(dis));
memset(du,0,sizeof(du));
int u,v;
q.push(s);
dis[s]=d;
vis[s]=1;
while(!q.empty()){
u=q.front(),v;
q.pop();
vis[u]=0;
for(int i=head[u];i;i=h[i].next){
v=h[i].to;
if(dis[v]<dis[u]-h[i].w+d){
dis[v]=dis[u]-h[i].w+d;
du[v]++;
if(du[v]>c){
cout<<"-1"<<endl;
exit(0);
}
if(!vis[v]){
vis[v]=1;
q.push(v);
}
}
}
}
for(int i=1;i<=c;i++)
Ans=max(Ans,dis[i]);
cout<<Ans<<endl;
}
int main(){
d=gi();p=gi();c=gi();f=gi();s=gi();
for(int i=1,u,v;i<=p;i++){
u=gi(),v=gi();
Add(u,v,0);
}
for(int i=1,u,v,w;i<=f;i++){
u=gi(),v=gi(),w=gi();
Add(u,v,w);
}
spfa();
return 0;
}
3.矿场重建
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。