图论复习

图论算法复习QwQ

最短路问题及其相关

Floyd及其相关

floyd适用于稠密图,存图方式是邻接矩阵,每个点到其他所有点都有存边。
至于floyd的k为什么要在最外层,是因为Floyd算法的本质是DP,而k是DP的阶段,因此要写最外面。
floyd的DP本质:
其原理是经过k点是否可以缩短路径。
原转移方程是这样的:
f[k][i][j]表示i到j之间通过编号k松弛出的最短路,f[0][i][j]为原图的临界矩阵。
这个状态设计有三维,
f[k][i][j]=min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]),前者表示不从k点走,后者表示从k点走,因为f[k]只与f[k-1]有关,所以就省略了。

模板:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=1001;
const int maxm=1000*1000+1;//边数,即点数的平方
const int inf=1<<29;//求最大值时 是-inf
int n,m,a[maxm],f[maxn][maxn],ans,u,v,w;
void init(){
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(i==j) f[i][j]=0;
            else f[i][j]=inf;
}
void floyd(){
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
}
int main()
{
    scanf("%d%d",&n,&m);
    init();//记得初始化
    for(int i=1;i<=n;i++) //邻接矩阵
        for(int j=1;j<=n;j++){
            scanf("%d",&w);
            f[i][j]=mid(w,f[i][j]);//注意判断一下充边,避免第二次相等u,v间的大边覆盖小边
        }
    floyd();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            cout<<f[i][j];//输出各个两点间的最短路
    return 0;
}
用途
1.求每两个点的最短路

附一个简单的例题,[USACO08OPEN]寻宝之路Clear And Present Danger]
求要求的路径上两相邻点的最短路的和,AC代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxm=10001;
const int maxn=101;
const int inf=100000*2;
int n,m,a[maxm],f[maxn][maxn],ans,u,v,w;
void floyd(){
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                f[i][j]=min(f[i][k]+f[k][j],f[i][j]);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d",&a[i]); a[0]=1;
    for(int i=1;i<=n;i++) 
        for(int j=1;j<=n;j++)
            {   scanf("%d",&w);f[i][j]=w;}
    floyd();
    for(int i=1;i<=m;i++)   ans+=f[a[i-1]][a[i]];
    cout<<ans;
}
2.求传递闭包

即判断两个点的联通性:f[i][j]=f[i][j]|(f[i][k]&f[k][j]);当f[i][j]=true或者是f[i][k]=true&&f[k][j]=true时,f[i][j]=true,即联通。

例题 P2419 [USACO08JAN]牛大赛Cow Contest
求每个点以其他点的联通性,如果都联通或间接联通,这个点可以确定排名。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxm=4501;
const int maxn=101;
int n,m,f[maxn][maxn],ans,u,v,cnt[maxn];
void floyd(){
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                f[i][j]=f[i][j]|(f[i][k]&f[k][j]);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d",&u,&v);f[u][v]=1;
    }
    floyd();
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    if(f[i][j]) cnt[i]++,cnt[j]++;//记录联通量
    for(int i=1;i<=n;i++)
    if(cnt[i]==n-1) ans++;//联通量到n-1时,与其他所有点的关系都有,可确定
    cout<<ans;
}
3.实时加入某些点,实时求加入这些点后的两点间的最短路

核心代码:

void floyd() {
    for(int k = 1; k <= n; k++) {  //可以根据题目所说的顺序转移
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                f[i][j] = min(f[i][k] + f[k][j], f[i][j]);
            }
        }
        //此处输出经过k个点后的最短路
    }
}

例题
从cf上截的图:
这里写图片描述
这里写图片描述

Taunt:
首先将删点改为加点
设新加入的点为x,则我们枚举u,v(都是从1到n),然后更新
a[u][v] = min(a[u][v], a[u][x] + a[x][v]);
这样一来只要计算答案时,枚举的u,v都是当前已经加入的点,就能保证计算的所有最短路经过的点都是当前存在的点。

#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long

const int maxn = 600;
int n;
ll a[maxn][maxn],p[maxn],sum[maxn];

int main() {
    cin>>n;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            cin>>a[i][j];
        }
    }
    for(int i = 1; i <= n; i++)  cin>>p[n - i + 1]; 
    for(int k = 1; k <= n; k++) {
        ll ans = 0;
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                a[p[i]][p[j]]=min(a[p[i]][p[k]]+a[p[k]][p[j]], a[p[i]][p[j]]);
                if(i <= k && j <= k) ans+=a[p[i]][p[j]];
            }
        }
        sum[n-k + 1] = ans;
    }
    for(int i =1; i <= n; i++) cout<<sum[i]<<' ';
    return 0;
}
4.找最小环,并输出路径

Taunt:
求图中的最小环。先考虑无向图。
Floyd算法保证了最外层循环到k的时候所有点对之间的最短路只经过1∼k−1号节点。
环至少有3个节点,设编号最大的为x,与之直接相连的两个节点为u和v。
环的长度应为f[u][v][x-1]+w[v][x]+w[x][u]。其中w为边权,如不存在边则为无穷大。
我们只要在进行第x次迭代之前枚举所有编号小于k的点对更新答案即可。

int ans = INF;
for(int k = 1; k <= n; k++) {
    for(int i = 1; i < k; i++) {
        for(int j = i+1; j < k; j++) {
            ans = min(ans, f[i][j] + a[j][k] + a[k][i]);
        }
    }
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= n; j++) {
            f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
        }
    }
}

例题[poj1734]Sightseeing trip

5.统计路径数

P2047 社交网络
思路:先用floyd ,如果通过k松弛的边与原先ij间最短路相等。则sum[i][j]+=sum[i][k]*sum[k][j];
如果松弛成功则需要重现计数当前松弛下来的最短路sum[i][j]=sum[i][k]*sum[k][j];
最后用一个p[maxn]数组,跑一边floyd,记录每次k是否是松弛成功的,用一遍变动的ans记录一下,最后除一下总最短路数sum[i][j]加到p[k]中。
代码:

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=201;
typedef long long ll;
int mp[maxn][maxn],n,m;
ll sum[maxn][maxn];
double p[maxn],ans;
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            mp[i][j]= 1<<29;
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        mp[u][v]=w; mp[v][u]=w;
        sum[u][v]=1; sum[v][u]=1;
    }
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++){
                if(mp[i][j]==mp[i][k]+mp[k][j])
                    sum[i][j]+=sum[i][k]*sum[k][j];
                else if(mp[i][j]>mp[i][k]+mp[k][j])
                    mp[i][j]=mp[i][k]+mp[k][j], sum[i][j]=sum[i][k]*sum[k][j];
            }
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++){
                if(i!=k&&i!=j&&j!=k){
                    ans=0;
                    if(mp[i][j]==mp[i][k]+mp[k][j])
                        ans=sum[i][k]*sum[k][j];
                    p[k]+=(double) ans/sum[i][j];
                }
            }
        printf("%.3lf\n",p[k]);
    }
    return 0;
}
6.输出i到j的最短路的路径

添加一个矩阵p,p[i][j]表示i到j的最短行径中的j之前的第1个点

void floyd() {
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            p[i][j] = j;
        }
    }
    for(int k = 1; k <= n; k++) {
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                if(f[i][k] + f[k][j] < f[i][j]) {
                    f[i][j] = f[i][k] + f[k][j];
                    p[i][j] = p[i][k];
                }
            }
        }
    }
    //打印路径
    for(int i = 1; i <= n; i++) {
        for(int j = i + 1; j <= n; j++) {
            int t = i;
            while(t != j) {
                cout<<t<<"->";
                t = p[t][j];    
            }
            cout<<j<<endl;
        }
    }
}

SPFA及其相关

模板:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=1e4+1;
const int maxm=5*1e5+1;
const int inf=2147483647;
int n,m,s,head[maxn],tot,vis[maxn],dis[maxn];
int read(){
    int t=1,x=0; char ch=getchar();
    while(ch>'9'||ch<'0'){if(t=='-') t=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
    return t*x;
}
struct edge{
    int u,v,w,next;
}e[maxm];
void add(int x,int y,int z){
    e[++tot]=(edge){x,y,z,head[x]};
    head[x]=tot;
}
void spfa(int x){
    for(int i=1;i<=n;i++) dis[i]=inf;
    vis[x]=1;   dis[x]=0;
    queue <int> q;
    q.push(x);
    while(!q.empty()){
        int k=q.front(); q.pop();
        vis[k]=0;
        for(int i=head[k];i;i=e[i].next){//i=head[k]    不是 i=head[x] 
            int to=e[i].v;
            if(dis[to]>dis[k]+e[i].w){
                dis[to]=dis[k]+e[i].w;
                if(!vis[to]) vis[to]=1,q.push(to);
            }
        }

    }
}
int main()
{
    n=read(); m=read(); s=read();
    for(int i=1;i<=m;i++){
        int a=read(),b=read(),c=read();
        add(a,b,c);
    }
    spfa(s);
    for(int i=1;i<=n;i++) printf("%d ",dis[i]);
    return 0;
}
判断负环
bool spfa(int x) {
    vis[x] = 1;
    for(int i = head[x]; i; i = e[i].next) {
        int v = e[i].v;
        if(d[v] > d[x] + e[i].w;) {
            d[v] = d[x] + e[i].w;
            if(!vis[v]) {
                if(spfa(v)) return 1;
            }
            else return 1;
        }
    }
    vis[x] = 0;
    return 0;
}
记录前驱,打印最短路径:
    while(!q.empty()) {
        int k = q.front();
        q.pop();
        vis[k] = 0;
        for(int i = head[k]; i; i = e[i].next) {
            int v = e[i].v;
            if(d[v] > d[k] + e[i].w) {
                d[v] = d[k] + e[i].w;

                f[v] = k;  //hear

                if(!vis[v]) {
                    vis[v] = 1;
                    q.push(v);
                }
            }
        }
    }   
    //找父亲 输出u——>v的路径   如果想正输可以建个队列存起来
    do {
        cout<<v<<' ';
        v = f[v];
    }while(v != u);
    cout<<u<<' ';
最短路计数:

在spfa过程中加以记录就好:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=1000000+1;
const int maxm=2000000+1;
const int mod=100003;
int n,m,head[maxn],tot,vis[maxn],dis[maxn],cnt[maxn];
int read(){
    int t=1,x=0; char ch=getchar();
    while(ch>'9'||ch<'0'){if(t=='-') t=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
    return t*x;
}
struct edge{
    int u,v,next;
}e[maxm];
void add(int x,int y){
    e[++tot]=(edge){x,y,head[x]};
    head[x]=tot;
}
void spfa(int x){
    for(int i=1;i<=n;i++) dis[i]= 1<<29;
    vis[x]=1;   dis[x]=0;   cnt[x]=1;
    queue <int> q;
    q.push(x);
    while(!q.empty()){
        int k=q.front(); q.pop();
        vis[k]=0;
        for(int i=head[k];i;i=e[i].next){//i=head[k]    不是 i=head[x] 
            int to=e[i].v;
            if(dis[to]>dis[k]+1){
                dis[to]=dis[k]+1;
                cnt[to]=cnt[k];
                if(!vis[to]) vis[to]=1,q.push(to);
            }
            else if(dis[to]==dis[k]+1) cnt[to]=(cnt[to]+cnt[k])%mod;
        }

    }
}
int main()
{
    n=read(); m=read();
    for(int i=1;i<=m;i++){
        int a=read(),b=read();
        add(a,b); add(b,a); 
    }
    spfa(1);
    for(int i=1;i<=n;i++) printf("%d\n",cnt[i]%mod);
    return 0;
}

最小生成树及其相关

最小生成树基于贪心的原理,用并查集来判断,一个含有n个点的图树种,有n-1条边,那么用sort对各边升序排序,然后从外面(非当前所找图中)添加n-1边即是最小生成树,其中用并查集操作来判断添加的边(点)是否已联通即是不是已找的联通块中的,如果不是,才添加。

模板:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=5001;
const int maxm=200001;
int n,m,f[maxn],ans,sum,flag;
struct edge{
    int u,v,w;
}e[maxm];
bool cmp(edge a,edge b){ return a.w<b.w;};
int find(int x){
    return f[x]==x ? x :f[x]=find(f[x]);//f[x]==x 少打一个"=",wa声一片 
}
bool get_find(int x,int y){
    int get1=find(x),get2=find(y);
    if(get1!=get2){
        f[get1]=get2;   return true;
    }
    return false ;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
    sort(e+1,e+1+m,cmp);
    for(int i=1;i<=n;i++) f[i]=i;
    for(int i=1;i<=m;i++){
        if(get_find(e[i].u,e[i].v)){
            sum++; ans+=e[i].w;
        }
        if(sum==n-1) {
            flag=1; break;
        }
    }
    if(flag) printf("%d",ans);
    else printf("orz");
    return 0;
}

其相关问题:

蓝书P343.

图片:

次小生成树

新年趣事之游戏
思路:先找最小生成树,把生成的树存起来,(注意要用两个结构体!!!这里卡了我很久)再dfs枚举树上每两个点之间的最大边权s[u][v]。
然后就是跑for枚举了,枚举加入每条不在树上的边(vis[i]=0的),加入一条边后,在树上会形成一个环(画图看看就知道),删去加入边所连接的两点间的最大边权s[e[i].u][e[i].v],用加入的边来连接图,这样生成的树就是次小生成树。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=501;
const int maxm=250000+1;
int n,m,f[maxn],tot,cnt,sum,ans1,ans2,vis[maxm],now,s[maxn][maxn],head[maxn];
int read(){
    int t=1,x=0; char ch=getchar();
    while(ch>'9'||ch<'0'){ if(ch=='-') t=-1; ch=getchar();  }
    while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
    return t*x;
}
struct edge{    int u,v,w,next;}e[maxm];
struct node{    int u,v,w;}bok[maxm];

bool cmp(node a,node b){return a.w<b.w;};

void add(int x,int y,int z){
    e[++cnt]= (edge){x,y,z,head[x]};    head[x]=cnt;
}

int find(int x){  return f[x]==x ? x : f[x]=find(f[x]);}

bool get_find(int x,int y){
    int t1=find(x),t2=find(y);
    if(t1!=t2){
        f[t1]=t2;
        return true;
    }
    return false;
}

void kruskal(){
    for(int i=1;i<=n;i++) f[i]=i;
    sort(bok+1,bok+1+m,cmp);//最重要的贪心排序掉了? 
    for(int i=1;i<=m;i++){
        if(get_find(bok[i].u,bok[i].v)){//判断两点的联通 
            tot++; ans1+=bok[i].w;  vis[i]=1;
            add(bok[i].u,bok[i].v,bok[i].w);    add(bok[i].v,bok[i].u,bok[i].w);
        }
        if(tot==n-1) break;
    } 
}
void dfs(int x,int fa,int num){
    s[now][x]=num;
    for(int i=head[x];i;i=e[i].next)
        if(e[i].v!=fa)  dfs(e[i].v,x,max(num,e[i].w));
}
void second(){
    for(int i=1;i<=n;i++)
        now=i, dfs(i,0,0);
    ans2= 1<<29;
    for(int i=1;i<=m;i++)
        if(!vis[i]) ans2=min(ans2,ans1+bok[i].w-s[bok[i].u][bok[i].v]);
}
int main(){
    n=read();   m=read();
    for(int i=1;i<=m;i++)
        bok[i].u=read(),bok[i].v=read(),bok[i].w=read();    
    kruskal();
    printf("Cost: ");
    if(tot==n-1) printf("%d\n",ans1);
    else printf("-1");
    second();
    printf("Cost: ");
    if(ans2== 1<<29) printf("-1");
    else printf("%d",ans2);
    return 0;
}

Tarjan及其相关

树及其相关

树的遍历,简单的dfs:

void dfs(int now)
{
    deep[now]=deep[fa[now]]+1;
    sum[now]=value[now]; maxx[now]=value[now];
    for 遍历从now出发的每一条边,边到达的点是v
    if (v != fa[now])
    {
        fa[v]=now;
        dfs(v);
        sum[now]+=sum[v];  maxx[now]=max(maxx[now], maxx[v]);
    }
}

LCA:

LCA指最近公共祖先,即同是两个点祖先的点中,深度最深的点。

直接上模板:

解释很详细qwq

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=500000+3;
int n,m,s;//点个数,询问次数,根节点编号
int head[maxn],f[maxn][20],tot,deep[maxn];
int read(){
    int t=1,x=0;    char ch=getchar();
    while(ch>'9'||ch<'0'){if(ch=='-')t=-1; ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0'; ch=getchar();}
    return t*x;
}
struct edge{int u,v,next;}e[maxn<<1];
void add(int x,int y){  e[++tot]=(edge){x,y,head[x]}; head[x]=tot;}
void build(int p){
    for(int i=head[p];i;i=e[i].next){//以此为根往下遍历 
        int to=e[i].v;
        if(deep[to]==0){//深度为0即为未处理过 
            deep[to]=deep[p]+1; f[to][0]=p;//to的深度是其父节点p+1,即to跳2^0步到p 
            build(to);//继续遍历建树 
        }
    }
}
void init_jump(){
    for(int i=1;i<=19;i++)//刚打过数据,小心卡常数 
        for(int j=1;j<=n;j++) f[j][i]=f[f[j][i-1]][i-1];//以2的倍增方式跳,第j个点向上跳i次能达到的点就是其跳i-1次到达的点再跳i-1次 
}
int lca(int x,int y){
    if(deep[x]<deep[y]) swap(x,y);//如果深度x小,交换,让x为深点以便下一步处理 注意深度大的是在下面
    for(int i=19;i>=0;i--)
        if(deep[f[x][i]]>=deep[y])  x=f[x][i];//先处理让他们深度相等,如果跳2^i次不超过y,就跳。 
    if(x==y) return x;//特判 
    for(int i=19;i>=0;i--) //然后一起跳 
        if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i]; //如果他们跳完之后祖先不相等的话,就跳 ,注意这里是判点而不是深度 
    return f[x][0];//按这样跳下去,一等会跳到只要再跳一步就能到lca的位置 
}
int main(){
    n=read(); m=read(); s=read();
    for(int i=1;i<n;i++){//注意n-1条边 
        int x=read(),y=read();
        add(x,y);   add(y,x);
    }
    deep[s]=1;  build(s);   init_jump();//根节点深度为1,然后建树,然后初始化向上跳 
    for(int i=1;i<=m;i++){
        int x=read(),y=read();
        printf("%d\n",lca(x,y));
    }
    return 0;
}
用处?

我们之前说了,树上任意两点的路径都是唯一的。并且这条路径一定经过lca。
这样遇到和路径有关的问题(比如查询x到y路径的长度,查询x到y路径的节点权值最小值等等)就可以拆分成两部分:x到lca和y到lca。
对于拆分开的每一条路径的答案,我们可以用倍增数组维护,
举例:维护最小值
minn[i][j]表示从i开始往上蹦2^j,经过的所有点的权值最小值。
minn[i][0]=min(value[i],value[fa[i]])
minn[i][j]=min(minn[i][j-1],minn[fa[i][j-1][j-1])
查询的时候,在往上蹦的过程中,顺便维护一下答案。

树的前缀和

首先知道数列的前缀和: sum[i]表示a[1]~a[i]的和。

用处1:求i~j的和sum[j]-sum[i-1]

用处2:区间修改。设置一个change数组。当区间[i,j]上要加k时,我们令change[j]+=k,令change[i+1]-=k。如果我们对change数组求前缀和的话,前缀和sum_change[i]就是i这个位置变动的值


树的前缀和有两种
根路径前缀和sum2[i],指i到根节点所有节点的权值之和。
子树前缀和sum1[i],指i的子树(包括i本身)所有节点的权值之和。
这两种前缀和预处理都非常简单

树的前缀和用处

根路径前缀和,可以用来求路径节点权值和(配合lca食用)
假如要求x到y路径的权值和,x,y的lca是z。则可以用sum[x]+sum[y]-2sum[z]+value[z]

子树前缀和,可以用来做路径修改(也得配合lca食用)
设定一个修改数组change。如果要对x到y路径上的所有点权值+k,lca为z。那么change[x]+=k,change[y]+=k,change[z]-=k,change[fa[z]]-=k。这样如果最后对change[i]求前缀和的话,最后得到的结果就是i权值的修改量
特点:可以O(1)修改,但是只能一次查询(因为要求前缀和O(n))

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值