图论:割点割边、强联通分量、2-SAT问题

割点:对于一个连通图,删去某些点得到的图将不再连通,而删去该点集的任意子点集得到的图还连通,则该点集为原图的点割集。相似的,割边的定义也是一样。

暴力做法是枚举去掉的点/边,对剩余图的连通块计数。而tarjan算法借助深搜可以在线性时间内完成。tarjan算法核心的两个辅助数组,dfn[i]为节点i在搜索树中的序号,low[i]为节点i可以到达的编号最小的节点(通过非父子边)。在深搜过程中记录每个节点的dfn和low值,如果某个节点的子树节点,不能跨过该节点去访问到编号更小的节点,那么就可以说明该节点是割点。相似的,割边也是同样的处理过程。有额外情况,若根的子树数目大于1,则根也是割点

POJ3352,模型是:对于给定的连通图要添加最少的边使图变为双连通图。在求割点后,有同一割点的点,low值会相等,这时候类似于缩点。有一条规律:要使得任意一棵树,在增加若干条边后,变成一个双连通图,那么至少增加的边数 =( 这棵树总度数为1的结点数 + 1 )/ 2。于是对于缩点后的连通图,寻找度数为1的点的个数。因为这道题里是无向边,所以最后答案为(num+1)/2

//POJ3352
#include<stdio.h>
#include<vector>
#include<algorithm>
using namespace std;
int n,r,dfn[1005],low[1005],dep,deg[1005],ans;
vector<int> edge[1005];

void tarjan(int u,int fa){
    dfn[u]=low[u]=++dep;
    for(int i=0;i<edge[u].size();i++){
        int v=edge[u][i];
        if(!dfn[v]){
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
        }
        else if(v!=fa){
            low[u]=min(low[u],dfn[v]);
        }
    }
}
int main(){
    scanf("%d%d",&n,&r);
    for(int i=0;i<r;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        edge[a].push_back(b);
        edge[b].push_back(a);
    }
    tarjan(1,1);
    for(int i=1;i<=n;i++){
        for(int j=0;j<edge[i].size();j++){
            int v=edge[i][j];
            if(low[i]!=low[v]){
                deg[low[i]]++;
            }
        }
    }
    for(int i=1;i<=n;i++){
        if(deg[i]==1) ans++;
    }
    printf("%d\n",(ans+1)/2);
    return 0;
}

强连通是指在有向图中,任意点对都可以互相访问,也就是每个节点都可以访问其他节点,那么该图就是强连通的。在有向图中对强联通分量计数或确定的问题,就是强连通分量问题。

两种做法,一种是Kosaraju算法,通过两次深搜完成。第一次深搜,将访问到的点倒序放入栈中(先访问到的后进栈)。第二次访问,根据栈中点的顺序依次逆向图上深搜,每一次深搜就得到一个强连通分量的缩点。而且,这样得到的强连通分量正好是按照拓扑排序的。POJ2186

//POJ2186
#include<stdio.h>
#include<vector>
#include<stack>
#include<string.h>
using namespace std;
const int maxn=10005;
int n,m,cmp[maxn];
vector<int> g[maxn],rg[maxn];
bool vis[maxn];
stack<int> s;

void addedge(int a,int b){
    g[a].push_back(b);
    rg[b].push_back(a);
}
void dfs(int u){
    vis[u]=true;
    for(int i=0;i<g[u].size();i++){
        if(!vis[g[u][i]]) dfs(g[u][i]);
    }
    s.push(u);   //注意,倒序放入栈中
}
void rdfs(int u,int k){
    vis[u]=true;
    cmp[u]=k;    //记录当前节点所属强连通分量的编号,也可以记录当前强连通分量的大小
    for(int i=0;i<rg[u].size();i++){
        if(!vis[rg[u][i]]) rdfs(rg[u][i],k);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++){
        int a,b;
        scanf("%d%d",&a,&b);
        addedge(a,b);
    }

    for(int i=1;i<=n;i++){
        if(!vis[i]) dfs(i);
    }
    memset(vis,false,sizeof(vis));
    int k=0;   //强连通分量计数
    while(!s.empty()){
        int u=s.top();
        s.pop();
        if(!vis[u]) rdfs(u,k++);
    }

    int ans=0,u=0;
    for(int i=1;i<=n;i++){
        if(cmp[i]==k-1){
            ans++;
            u=i;
        }
    }
    memset(vis,false,sizeof(vis));
    rdfs(u,0);   //找到最后一个强连通分量,在逆向图上遍历寻找不能到达的点
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            ans=0;
            break;
        }
    }
    printf("%d\n",ans);
}

第二种是Tarjan做法,思想和判断割点的相似,但是代码上有些细节之处不同。特点是避免了二次深搜,不用建反向图。因为强连通分量的题目里的原图一般是有多个连通分量,所以可能需要多次深搜。POJ3180

POJ1236,最后要求入度为0的强连通分量个数,以及要使所连边的个数。对于后者,答案应该是入度为0的强连通分量个数和出度为0的强连通个数的最大值。入度为0和出度为0的强连通分量相互连接的情况下得到要额外添加的最少边。

//POJ1236
#include<stdio.h>
#include<vector>
#include<algorithm>
#include<stack>
#include<string.h>
using namespace std;
const int maxn=1005;
int n,num,ans,dep,cnt,indeg[maxn],outdeg[maxn],dfn[maxn],low[maxn],cmp[maxn];
bool vis[maxn];
vector<int> edge[maxn];
stack<int> s;

void tarjan(int u){
    dfn[u]=low[u]=++dep;
    vis[u]=true;
    s.push(u);
    for(int i=0;i<edge[u].size();i++){
        int v=edge[u][i];
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v]){   //注意,和割点代码不同
                low[u]=min(low[u],dfn[v]);
        }
    }
    if(low[u]==dfn[u]){
        cnt++;
        int v;
        do{
            v=s.top();
            vis[v]=false;    //务必注意
            s.pop();
            cmp[v]=cnt;
        }while(u!=v);
    }
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        int tmp;
        while(scanf("%d",&tmp)!=EOF){
            if(tmp==0) break;
            edge[i].push_back(tmp);
        }
    }

    for(int i=1;i<=n;i++){
        if(!dfn[i]) tarjan(i);
    }
    for(int i=1;i<=n;i++){
        for(int j=0;j<edge[i].size();j++){
            int v=edge[i][j];
            if(cmp[i]!=cmp[v]){
                outdeg[cmp[i]]++;
                indeg[cmp[v]]++;
            }
        }
    }
    int ans1=0,ans2=0;
    for(int i=1;i<=cnt;i++){
        if(indeg[i]==0) ans1++;
        if(outdeg[i]==0) ans2++;
    }
    if(cnt==1) printf("1\n0\n");
    else printf("%d\n%d\n",ans1,max(ans1,ans2));
}

2-SAT问题,每个物体有两种互斥的选择,不同物体之间的选择有二元关系。将题目中的二元关系转化为有向图,a->b这样的一条边表示有a必有b;对于一元关系必须有a,则建边a'->a,表示没有a则必须有a。这样建图之后,如果a与a'在同一个联通分量里,则说明当前情况是不存在的。

必须要强调,2-SAT问题本质上是用于判断当前情况可不可行的,可行情况下得到的解也只是一种特解,所以建图时集中于矛盾的情况。也就是说,添加的边是当前二元关系出现矛盾时的边。从逻辑上说,二元关系也就是且、或、异或这几种,因此找到矛盾关系后直接套这三个关系的模板就行。模板是POJ3678

编码上的话,提醒一下注意图中点的个数(因为有正负两个点,所以代码中点的个数一般是题目中给出的点个数的2倍)。2-SAT本质上是判断问题,所以当求最优解时,用二分的方法,将构造性问题转为判定性问题

//POJ3678
#include<stdio.h>
#include<vector>
#include<stack>
using namespace std;
const int maxn=2005;
int n,m,cnt,dep,V,low[maxn],dfn[maxn],cmp[maxn];
bool vis[maxn];
vector<int> edge[maxn];
stack<int> s;

void addedge(int a,int b){
    edge[a].push_back(b);
}
void tarjan(int u){
    low[u]=dfn[u]=++dep;
    vis[u]=true;
    s.push(u);
    for(int i=0;i<edge[u].size();i++){
        int v=edge[u][i];
        if(!dfn[v]){
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v]){
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(low[u]==dfn[u]){
        int v;
        cnt++;
        do{
            v=s.top();
            s.pop();
            vis[v]=false;
            cmp[v]=cnt;
        }while(u!=v);
    }
}
void ssc(){
    for(int i=0;i<V;i++){
        if(!dfn[i]) tarjan(i);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    V=2*n;
    for(int i=0;i<m;i++){
        int a,b,c;
        char str[5];
        scanf("%d%d%d%s",&a,&b,&c,str);
        if(str[0]=='A'){
            if(c==1){   //两者必须为1,若为0则必为1
                addedge(a+n,a);addedge(b+n,b);
            }else if(c==0){   //不同时为1,我若为1你必为0
                addedge(a,b+n);addedge(b,a+n);
            }
        }
        else if(str[0]=='O'){
            if(c==1){   //不同时为0,我若为0你必为1
                addedge(a+n,b);addedge(b+n,a);
            }
            else if(c==0){   //两者必须为0,若为1则必为0
                addedge(a,a+n);addedge(b,b+n);
            }
        }
        else if(str[0]=='X'){
            if(c==1){   //两者不同,我为0你为1,我为1你为0
                addedge(a,b+n);addedge(a+n,b);
                addedge(b,a+n);addedge(b+n,a);
            }
            else if(c==0){   //两者相同,我为1你为1,我为0你为0
                addedge(a,b);addedge(a+n,b+n);
                addedge(b,a);addedge(b+n,a+n);
            }
        }
    }
    ssc();

    for(int i=0;i<n;i++){
        if(cmp[i]==cmp[i+n]){
            printf("NO\n");
            return 0;
        }
    }
    printf("YES\n");
}

对于有实际情景的题目来说,关键就在于把握矛盾关系。之后套模板就行。POJ3683POJ2723POJ2749。在POJ3683里面,出现了判断区域是否矛盾的方法。对于配合二分求解的题目,冲突关系一般不在前置的条件中,前置条件直接建边即可,冲突关系一般是在不同二分条件下视情况添加一定的冲突关系

//POJ3683
#include<stdio.h>
#include<vector>
#include<stack>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn=2005;
int n,V,s[maxn],t[maxn],d[maxn],cmp[maxn];
bool vis[maxn];
vector<int> edge[maxn],redge[maxn];
stack<int> st;

void addedge(int a,int b){
    edge[a].push_back(b);
    redge[b].push_back(a);
}
void dfs(int u){
    vis[u]=true;
    for(int i=0;i<edge[u].size();i++){
        int v=edge[u][i];
        if(!vis[v]) dfs(v);
    }
    st.push(u);
}
void rdfs(int u,int k){
    vis[u]=true;
    cmp[u]=k;
    for(int i=0;i<redge[u].size();i++){
        int v=redge[u][i];
        if(!vis[v]) rdfs(v,k);
    }
}
void ssc(){
    for(int i=1;i<=V;i++){
        if(!vis[i]) dfs(i);
    }
    memset(vis,false,sizeof(vis));
    int k=0;
    while(!st.empty()){
        if(!vis[st.top()]) rdfs(st.top(),++k);
        st.pop();
    }
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        int a,b,c,e;
        scanf("%d:%d%d:%d %d",&a,&b,&c,&e,&d[i]);
        s[i]=a*60+b;t[i]=c*60+e;
    }
    V=2*n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<i;j++){
            if(min(s[i]+d[i],s[j]+d[j])>max(s[i],s[j])){
                addedge(i,j+n);
                addedge(j,i+n);
            }
            if(min(s[i]+d[i],t[j])>max(s[i],t[j]-d[j])){
                addedge(i,j);
                addedge(n+j,n+i);
            }
            if(min(t[i],s[j]+d[j])>max(t[i]-d[i],t[j])){
                addedge(n+i,n+j);
                addedge(j,i);
            }
            if(min(t[i],t[j])>max(t[i]-d[i],t[j]-d[j])){
                addedge(i+n,j);
                addedge(j+n,i);
            }
        }
    }
    ssc();

    for(int i=1;i<=n;i++){
        if(cmp[i]==cmp[i+n]){
            printf("NO\n");
            return 0;
        }
    }
    printf("YES\n");
    for(int i=1;i<=n;i++){
        if(cmp[i]>cmp[i+n]){
            printf("%02d:%02d %02d:%02d\n",s[i]/60,s[i]%60,(s[i]+d[i])/60,(s[i]+d[i])%60);
        }
        else{
            printf("%02d:%02d %02d:%02d\n",(t[i]-d[i])/60,(t[i]-d[i])%60,t[i]/60,t[i]%60);
        }
    }
    return 0;
}

 

posted @ 2020-10-09 13:02  太山多桢  阅读(267)  评论(0)    收藏  举报