图论篇部分模板(基于链式前向星的存储方式)
你还在为暴力枚举复杂度太高而苦恼吗?你还在为DP而痛苦吗?你还在为搜索剪枝而绞尽脑汁吗?选择链式前向星吧,链式前向星——专注存图20年 。
1. 链式前向星
int e[maxn], ne[maxn], h[N],weight[maxn],idx;
int in_degree[N];
void add(int u, int v, int w) {
e[idx] = v, ne[idx] = h[u], h[u] = idx++;//前插链表
}
2. dfs
void dfs(int u, int fa) {
for (int i = h[u]; i != -1; i = ne[i]) {
int t = e[i];
if (t != fa) {
dfs(t, i);
}
}
}
3. bfs
void bfs(int root) {
queue<int> q;
q.push(root);
while (!q.empty()) {
int t = q.front(); q.pop();
for (int i = h[t], i != -1, i = ne[i]) {
q.push(e[i]);
}
}
}
4. 拓扑排序(判断有无环)
bool vis[N];
void top_sort(int n) {//n个节点
for (int i = 0; i < n; i++) {
if (in_degree[i] == 0)
q.push(i);
}
if (!q.size()&&n!=0) puts("有环");
while (!q.empty()) {
int t = q.front(); q.pop();
vis[t] = true;
for (int i = h[t], i != -1; i++){
int k = e[i];
if (--in_degree[k] == 0) {
q.push(k);
}
}
}
for (int i = 0; i < n; i++) {
if (!vis[i]) puts("有环");
}
}
5. 欧拉路
无向图所有度节点均为偶数才存在欧拉回路。uva10054
6.无向图的连通性
无向图的连通性
割点:
定义dfn[u]
:记录dfs
对每个点的访问顺序,dfn
随着地推深度的增加而变大
定义low[i]
:记录v
和v
的后代能连回的祖先的num
只要low[v]>=dfn[u]
,就说明u
是割点,对于v
无其他回退边
只要将条件改为low[v]>dfn[u]
,即是判断割边的条件,表示v
只能回退到u
常用数组 dfn
,_time
-时间戳,dfn
顺序赋值,low
数组,回退赋值
int dfn[maxn], low[maxn];
int _time;
int cnt;
void tarjan_V(int u,int fa) { //割点
dfn[u] = low[u] = ++_time;
cnt = 0;
for (int i = h[u]; i != -1; i=ne[i]) {
int v = e[i];
if (!dfn[v]) {
cnt++;
tarjan_V(v, u);
low[u] = min(low[u], low[v]);
if (dfn[u] <= low[v]&&u!=1) { //不是根
printf("%d是割点\n",u);
}
}
else if (dfn[v] < dfn[u] && v != fa) {
low[u] = min(low[u], dfn[v]);
}
}
if (u == 1 && cnt > 1) {
printf("%d是割点\n", u);
}
}
void tarjan_E(int u, int fa) { //割边
dfn[u] = low[u] = ++_time;
for (int i = h[u]; i != -1; i = ne[i]) {
int v = e[i];
if (!dfn[v]) {
tarjan_E(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
printf("%d---%d是割边", u, v);
}
}
else if (dfn[v] && v != fa) { //v被遍历过
low[u] = min(low[u], dfn[v]);
}
}
}
6. 最短路
图的规模较小:Floyd
,如果边的权值有负数,需要判断负圈。(n<200)
n*m<107 : B-F;
图的规模较大,且边的权值非负数,用Dijstra
,更大
图的规模大,且边的权值有负数,用SPFA,需要判断负圈。更大
Floyd(O(n3))
能一次性求所有节点的最短路,只能适用于小规模的图。区间DP。
memset(graph,INF,sizeof graph);
void floyd(){
int s=1; //定义起点
for(int k=1;k<=n;k++) //先枚举k,保证后续枚举有意义。
for(int i=1;i<=n;i++){
if(graph[i][k]!=INF){
for(int j=1;j<=n;j++){
if(graph[k][j]!=INF)
if(graph[i][j]>graph[i][k]+graph[k][j])
graph[i][j]=graph[i][k]+graph[k][j];
}
}
}
printf("%d",graph[s][n]);
}
判断负圈
graph[i][i]
最终是绕一圈之后的最小路径,如果graph[i][i]<0
就表明有负圈,此时可置graph[i][i]=0
这样可以加快判断过程。
Luogu1870最长路:(不建议floyd
,数据范围较大)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1501;
int graph[N][N];
int n,m,x,y,z;
#define 0x3f INF
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i==j) graph[i][j]=0;
else graph[i][j]=INF;
for(int i=1;i<=m;i++){
cin>>x>>y>>z;
graph[x][y]=min(graph,-z);
}
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++){
if(graph[i][k]!=INF)
for(int j=1;j<=n;j++)
if(graph[k][j]!=INF)
if(graph[i][k]+graph[k][j]<graph[i][j])
graph[i][j]=graph[i][k]+graph[k][j];
}
}
if(graph[1][n]==INF) cout<<-1;
else cout<<-graph[1][n];
return 0;
}
B-F(O(n*m))
解决单源最短路径问题。
给定一个起点s,求它到图中所有n个节点的最短路径.。
每一轮给所有n个人每人一次机会,问他的邻居到s的最短距离是多少,如果他的邻居到s的距离不是INF,他就可以借道他的邻居到s,以更新自己到s的距离,一共需要n次松弛
非优化存储版:采用的邻接矩阵,不得不遍历n*n条边
void bellman(){
int s=1;//定义起点
int d[maxn];
for(int i=1;i<=n;i++)
d[maxn]=INF;
d[s]=0;
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(d[j]>d[i]+graph[i][j])
d[j]=d[i]+graph[i][j];
printf("%d\n",d[n]);
}
换用结构体存储
struct edg{int u,v,w;}e[10005];
void bellman(){
int s=1;//定义起点
int d[maxn];
for(int i=1;i<=n;i++)
d[maxn]=INF;
d[s]=0;
for(int k=1;k<=n;k++)
for(int i=0;i<cnt;i++){ //cnt是存储的边的数量
int x=e[i].u,y=e[i].v;
if(d[y]>d[x]+e[i].w){
d[y]=d[x]+e[i].w;
pre[y]=x; //如果有需要记录路径
}
}
printf("%d\n",d[n]);
}
判断负圈
当没有负圈时,只需要n轮就结束,如果超过n轮,最短路径还有变化,那么肯定有负圈。(加上一个负数,一定在一直减少)。
void bellman(){
int s=1;//定义起点
int d[maxn];
for(int i=1;i<=n;i++)
d[maxn]=INF;
d[s]=0;
bool update=true;int k=0;
while(update){
k++;
update=false;
if(k>n){ //超过了n轮操作
printf("有负圈\n");
return;
}
for(int i=0;i<cnt;i++){ //cnt是存储的边的数量
int x=e[i].u,y=e[i].v;
if(d[y]>d[x]+e[i].w){
d[y]=d[x]+e[i].w;
pre[y]=x; //如果有需要记录路径
}
}
printf("%d\n",d[n]);
}
SPFA (不稳定)
用队列处理Bellman-Floyd,单源最短路径
- 存图采用邻接表。
struct edg{
int from,to,w;
edg(int a,int b,int c){
form=a,to=b,w=c;
}
}
vector<edg>e[maxn]; //输入以from做为下标存储
int n,m;
int pre[maxn];
int spfa(int s){
int dis[maxn];
int neg[maxn];
bool vis[maxn];//vis表示当前节点是否在队列中
memset(vis,0,sizeof vis);
vis[s]=1;
memset(dis,INF,sizeof dis);
dis[s]=0;
queue<int>q;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u]=false;
for(int i=0;i<e[u].size();i++){
int v=e[u][i].to,w=e[u][i].w;
if(dis[u]+w<dis[v]){
dis[v]=dis[u]+w;
pre[v]=w; //记录路径
if(!vis[v]){ //该状态更新了,需要放入队列
vis[v]=1;
q.push(v);
neg[v]++;
if(neg[v]>n) return 1; //出现负圈,有点被松弛的次数大于n
}
}
}
}
printf("%d",dis[n]);
return 0;
}
- 如果图依旧较大,采用链式前向星存储
int h[maxn],ne[2*maxn],idx,to[2*maxn],w[2*maxn];
void add(int u,int v,int wight){
to[idx]=v,ne[idx]=h[u],w[idx]=weight,h[u]=idx++;
}
int spfa(int s){
int dis[maxn],neg[maxn],vis[maxn];
for(int i=1;i<=n;i++) dis[i]=INF,vis[i]=false,neg[i]=0;
dis[s]=0;
queue<int>q;
q.push(s);
vis[s]=1;
while(!q.empty()){
int u=q.front();q.pop();
for(int i=h[u];i!=-1;i=ne[i]){ //遍历邻居
int t=to[i];
if(dis[t]>dis[u]+w[i]){
dis[t]=dis[u]+w[i];
pre[t]=u;
if(!vis[t]){
q.push(t);
neg[t]++;
if(neg[t]>n){
printf("出现负圈\n");
return 0;
}
}
}
}
}
printf("%d\n",dis[n]);
return 0;
}
Dijkstra(不能有负权边)(O(m*log2n))
解决单源最短路径问题。
维护两个集合A(已经确定的最短路径的集合),B(扩展的邻居的集合),每次从B中选择出距离起点s最近的v,将v加入集合A,B中去掉v并将v的邻居加入集合B,这样重复,直至B为空。
bool done[maxn]
数组记录当前节点是否被处理或者在集合A中,也可用于舍弃B中不优的选择
eg:B={2_3,2_4}
舍弃2_4
。 2_3
:表示起点到2的距离为3,两种信息,当前节点以及距离
int h[maxn],ne[2*maxn],idx,to[2*maxn],w[2*maxn];
bool done[maxn];
void add(int u,int v,int wight){
to[idx]=v,ne[idx]=h[u],w[idx]=weight,h[u]=idx++;
}
struct node{ //存节点信息
int id,n_dis;//id:节点,n_dis:这个节点到起点的距离
node(int b,int c){id=b,n_dis=c;}
bool operator<(const node&a)const{
return n_dis>a.n_dis;// 从小到大
}
};
void dijkstra(){
int s=1; //定义起点
int dis[maxn]; //dis相当于集合A
bool done[maxn];
for(int i=1;i<=n;i++) dis[i]=INF,done[i]=false;
dis[s]=0;
priority_queue<node> q; //集合B
q.push(node(s,dis[s]));
while(!q.empty()){
node u=q.top();q.pop();
if(done[u.id]) continue;//已经被舍弃或者加入A的id
done[u.id]=true;
for(int i=h[u.id],i!=-1;i=ne[i]){ //遍历邻居
int y=to[i]; //y就是邻居
if(done[y]) continue; //被舍弃的情况
if(dis[y]>w[i]+u.n_dis){ //未被舍弃并且可以更新。
dis[y]=w[i]+u.n_dis;
q.push(node(y,dis[y]));
pre[y]=u.id;
}
}
}
printf("%d",dis[n]);
}
7.最小生成树
针对无向图——连通所有节点的情况下保持路径和最短。
hdu1233.
Problem Description:某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。
Prim算法(O(E*log2V))
维护一个集合U表示已知的点,每次将距离集合U中所有点的最近的点加入U,优先队列优化O(E*log2V)。
int dis[maxn];// 存距离
bool vis[maxn];// 是否在队列中
struct node{
int id,w;
node(int a,int b){
id=a,w=b; //id表示边的编号
}
bool operator<(const node&a)const{
return w>a.w;// 从小到大
}
}edg[maxn*2];
int prim(int s){
int ans=0;
priority_queue<Edg>q;
//加入起点的边
for(int i=h[s];i!=-1;i=ne[i]){
if(!vis[to[i]]){
q.push(new node{i,w[i]});
}
}
while(!q.empty()){
node t=q.front();q.pop();
ans+=t.w;
int y=to[t];
vis[y]=true;
int min_x=cc=0,min_num=INF;
for(int i=h[y];i!=-1;i=ne[i]){
int x=to[i],we=w[i];
if(!vis[x]){ //不成环
if(min_num>we) we=min_num,min_cc=x;
q.push(new node{i,we}); //如果未被遍历过就加入队列
}
}
}
}
Kruskal算法(O(E*log2E))
- 对边进行排序(边是集合,不能分开算——用结构体算(存下标和权值也可)),依次将最短的边加入T中
- 判断圈,可以选用并查集
int parent[maxn],depth[maxn];//并查集
struct Edg{int u,v,w}edg[maxn*maxn];
bool cmp(const Edg&a,const Edg&b){return a.w<b.w;}
int find(int x){
return x==parent[x]?parent[x]:x=find(parent[x]);
}
void Union(int x,int y){
int rootx=find(x),rooty=find(y);
if(rootx==rooty) return;
if(depth[rootx]>dept[rooty]) parent[rooty]=rootx;
else{
parent[rootx]=rooty;
if(depth[rootx]==depth[rooty]) depth[rooty]++;
}
}
int kruskal(){
int ans=0;
for(int i=1;i<=n;i++)
parent[i]=i; //初始化,开始每个村庄都是单独的集,并查集
sort(edg+1,edg+1+m,cmp);// m条边
for(int i=1;i<=m;i++){
int b=find(edg[i].u),c=find(edg[i].v);
if(b==c) continue;
else ans+=edg[i].w;
}
return ans;
}
8最大流
网络流的基本问题,原型是水流问题。
Ford-Fulkerson方法:(复杂度很高,只能用于小图)
求图G中最多有多少条不相交的s->t路径:
S-T割:
- 找到一个大小为
l
的S-T割
k<=l
,当l
为0时表示不存在路径
-
找到m条不相交路径
m<=k
求解:如果我们能同时找到m条不相交路径和大小为m的S-T割,则k=m
hdu 1532
Edmonds-Karp算法:
Problem Description:一个有向图,求1到N的最大流。
#include<iostream>
using namespace std;
#define INF 0x3f
const int maxn=300;
int n,m,graph[maxn][maxn],pre[maxn]; //graph 不仅记录图,还是残留网格 , n是边数,m是节点数
int bfs(int s,int t){
int flow[maxn];
memset(pre,-1,sizeof pre);
flow[s]=INF,pre[s]=0;//初始化起点
queue<int> q;q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
if(u==t) break; //搜索到一个路径
for(int i=1;i<=m;i++){ //BFS所有的点
if(i!=s&&graph[u][i]>0&&pre[i]==-1){ //graph[u][i]>0表明这条路可用
pre[i]=u; //记录路径
q.push(i);
flow[i]=min(flow[u],graph[u][i]); //更新节点流量
}
}
}
if(pre[t]==-1) return -1; //没有找到新的路径
return flow[t]; //返回这个增广路径的流量
}
int maxflow(int s,int t){
int ans=0;
while(1){
int flow=bfs(s,t); //执行一次bfs,找到一条路径
if(flow==-1) break;
int cur=t;
while(cur!=s){
int father=pre[cur]; //pre[]记录路径上前一个点
graph[father][cur]-=flow; //更新残留网络
graph[cur][father]+=flow; //反向
cur=father;
}
ans+=flow;
}
return ans;
}
int mian(){
while(~scanf("%d %d",&n,&m)){
memset(graph,0,sizeof graph);
for(int i=0;i<n;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
graph[u][v]+=w;
}
printf("%d\n",maxflow(1,m));
}
return 0;
}
- 记录一次错误,关闭流同步后不能混用
cout
和printf
,会出问题。
std::ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
printf("%d",1);
cout<<"222";