成为图论大师之路

本文来自《挑战程序设计竞赛》4.3 成为图论大师之路

1.强连通分量分解

1.定义

对于一个有向图顶点的子集S,如果在S内任取两点u和v,都能找到一条从u到v的路径,那么称S是强连通的。如果在强连通的顶点集合S加入其他任意顶点后,它都不再是强连通的,那么就称S是原图的一个强连通分量SCC(Strongly Connected Component)。任意有向图都可以分解成若干个不相交的强连通分量,这就是强连通分量分解。把分解后的强连通分量压缩成一个顶点,就得到了一个DAG。

2.解法原理

强连通分量分解可以通过两次简单的DFS实现。第一次DFS时,选取任意顶点作为起点,遍历所有尚未访问的顶点,并编号。对剩余的未访问过的顶点,重复上述过程。
完成标号后,越接近图的尾部(搜索树的叶子),顶点的标号越小。第二次DFS时,将所有边反向,然后以标号最大的顶点作为起点进行DFS,这样DFS所遍历的顶点集合就构成了一个强连通分量。只要还有未访问过的顶点,就从中选取标号最大的顶点不断重复上述过程。
可以发现,标号最大的点属于DAG头部(搜索树的跟)的强连通分量。因此将边反向后,就不能沿边访问到这个强连通分量以外的顶点,但是对于强连通分量以内的顶点,可达性不受影响。因此,在第二次DFS时,可以遍历一个强连通分量里的所有顶点。
总的时间复杂度为O(V+E)

3.代码

#include<iostream>
#include<vector>
#include<cstring>
#include<cstdio>
using namespace std;
//模板
const int MAX_V=1005;
int V;//顶点数
vector<int> G[MAX_V];//图的邻接表表示
vector<int> rG[MAX_V];//把边反向后的图
vector<int> vs;//后序遍历顺序的顶点列表
bool used[MAX_V];//访问标记
int cmp[MAX_V];//所属强连通分量的拓扑序
void add_edge(int from,int to)
{
    G[from].push_back(to);
    rG[to].push_back(from);
}
void dfs(int v)
{
    used[v]=true;
    for(int i=0;i<G[v].size();i++){
        if(!used[G[v][i]]){
            dfs(G[v][i]);
        }
    }
    vs.push_back(v);
}
void rdfs(int v,int k)
{
    used[v]=true;
    cmp[v]=k;
    for(int i=0;i<rG[v].size();i++){
        if(!used[rG[v][i]]){
            rdfs(rG[v][i],k);
        }
    }
}
int scc()
{
    memset(used,0,sizeof(used));
    vs.clear();
    for(int v=0;v<V;v++){
        if(!used[v]){
            dfs(v);
        }
    }
    memset(used,0,sizeof(used));
    int k=0;
    for(int i=vs.size()-1;i>=0;i--){
        if(!used[vs[i]]){
            rdfs(vs[i],k++);
        }
    }
    return k;
}
//模板
int main()
{
    cin>>V;
    int a,b;
    cin>>a>>b;
    while(a>=0&&b>=0){
        add_edge(a,b);
        cin>>a>>b;
    }
    cout<<scc()<<endl;
    return 0;
}

2.2-SAT

1.定义

给定一个布尔方程,判断是否存在一组布尔变量的真值指派使得整个方程为真的问题,被称为布尔方程的可满足性问题(SAT)。SAT问题是NP完全的,但对于满足一定限制条件的SAT问题,能够有效求解。我们将下面这种布尔方程成为合取范式。
(aVbV……)^(cVdV……)^……。其中a,b,c……称为文字,它是一个布尔变量或者其否定,像(aVbV……)用V连接的部分称为子句。如果合取范式的每个子句中的文字个数不超过两个,那么对应的SAT问题成为2-SAT问题。
利用强连通量分解,可以在布尔公式子句数的线性时间内解决2-SAT问题。
对于每个布尔变量x,构造两个顶点x,非x,以→关系为边建立有向图。此时,如果图上的a点能够到达b点,就表示a为真b也一定为真。因此图中同一个强连通分量重所含的所有文字的布尔值均相同。
如果存在某个布尔变量x,x和非x在同一个强连通分量中,则显然无法让整个布尔公式为真。
反之,如果不存在这样的布尔变量,对于每个布尔变量x,让
x所在的强连通分量的拓扑序在非x所在的强连通分量的拓扑序之后   等价于x为真。
就是使得该公式的值为真的一组合适的布尔赋值。

2.代码

#include<iostream>
#include<vector>
#include<cstring>
#include<cstdio>
using namespace std;
//模板
const int MAX_V=1005;
int V;//顶点数
vector<int> G[MAX_V];//图的邻接表表示
vector<int> rG[MAX_V];//把边反向后的图
vector<int> vs;//后序遍历顺序的顶点列表
bool used[MAX_V];//访问标记
int cmp[MAX_V];//所属强连通分量的拓扑序
void add_edge(int from,int to)
{
    G[from].push_back(to);
    rG[to].push_back(from);
}
void dfs(int v)
{
    used[v]=true;
    for(int i=0;i<G[v].size();i++){
        if(!used[G[v][i]]){
            dfs(G[v][i]);
        }
    }
    vs.push_back(v);
}
void rdfs(int v,int k)
{
    used[v]=true;
    cmp[v]=k;
    for(int i=0;i<rG[v].size();i++){
        if(!used[rG[v][i]]){
            rdfs(rG[v][i],k);
        }
    }
}
int scc()
{
    memset(used,0,sizeof(used));
    vs.clear();
    for(int v=0;v<V;v++){
        if(!used[v]){
            dfs(v);
        }
    }
    memset(used,0,sizeof(used));
    int k=0;
    for(int i=vs.size()-1;i>=0;i--){
        if(!used[vs[i]]){
            rdfs(vs[i],k++);
        }
    }
    return k;
}
//模板
int main()
{
    //布尔公式(aV~b)^(bVc)^(~cV~a)
    //构造6个顶点a,b,c,~a,~b,~c
    V=6;
    //aV~b
    add_edge(3,4);
    add_edge(1,0);
    //bVc
    add_edge(4,2);
    add_edge(5,1);
    //~cV~a
    add_edge(2,3);
    add_edge(0,5);
    //进行强连通分量分解
    scc();
    for(int i=0;i<3;i++){
        if(cmp[i]==cmp[i+3]){
            cout<<"No"<<endl;
            return 0;
        }
    }

    //如果可满足,输出一组解
    cout<<"Yes"<<endl;
    for(int i=0;i<3;i++){
        if(cmp[i]>cmp[i+3]){
            cout<<"true"<<endl;
        }
        else{
            cout<<"false"<<endl;
        }
    }
    return 0;
}

3.Priest John's Busiest Day(Poj 3683)

1.题目原文

Priest John's Busiest Day
Time Limit: 2000MS Memory Limit: 65536K
Total Submissions: 9292 Accepted: 3162 Special Judge

Description

John is the only priest in his town. September 1st is the John's busiest day in a year because there is an old legend in the town that the couple who get married on that day will be forever blessed by the God of Love. This year N couples plan to get married on the blessed day. The i-th couple plan to hold their wedding from time Si to time Ti. According to the traditions in the town, there must be a special ceremony on which the couple stand before the priest and accept blessings. The i-th couple need Di minutes to finish this ceremony. Moreover, this ceremony must be either at the beginning or the ending of the wedding (i.e. it must be either from Si to Si + Di, or from Ti - Di to Ti). Could you tell John how to arrange his schedule so that he can present at every special ceremonies of the weddings.

Note that John can not be present at two weddings simultaneously.

Input

The first line contains a integer N ( 1 ≤ N ≤ 1000). 
The next N lines contain the SiTi and DiSi and Ti are in the format of hh:mm.

Output

The first line of output contains "YES" or "NO" indicating whether John can be present at every special ceremony. If it is "YES", output another N lines describing the staring time and finishing time of all the ceremonies.

Sample Input

2
08:00 09:00 30
08:15 09:00 20

Sample Output

YES
08:00 08:30
08:40 09:00

Source


2.解题思路

对于每个结婚仪式,只有在开始或者结束时进行特别仪式两种选择,因此可以定义变量x[i]:x[i]为真时,在开始时进行特别仪式。
这样,对于结婚仪式i和j,如果S[i]-S[i]+D[i]和S[j]-S[j]+D[j]有冲突,则~x[i]V~x[j]为真,对于开始和结束、结束和开始、结束和结束等三种情况类似。于是要保证所有特别仪式的时间不冲突,只要考虑将这些子句用^连接起来所得到的布尔公式即可。对于,输入的样例,有布尔公式(~x1V~x2)^(x1V~x2)^(x`Vx2),在x1为真x2为假时,其值为真。这样,我们可以把原题转化成2-SAT问题。接下来只要进行强连通两分量分解并判断是否有使得布尔公式值为真的一组布尔变量赋值就可以啦。

3.AC代码

#include<iostream>
#include<vector>
#include<cstring>
#include<cstdio>
using namespace std;
//模板
const int MAX_N=1005;
int N;
int S[MAX_N],T[MAX_N],D[MAX_N];
const int MAX_V=2005;
int V;//顶点数
vector<int> G[MAX_V];//图的邻接表表示
vector<int> rG[MAX_V];//把边反向后的图
vector<int> vs;//后序遍历顺序的顶点列表
bool used[MAX_V];//访问标记
int cmp[MAX_V];//所属强连通分量的拓扑序
void add_edge(int from,int to)
{
    G[from].push_back(to);
    rG[to].push_back(from);
}
void dfs(int v)
{
    used[v]=true;
    for(int i=0;i<G[v].size();i++){
        if(!used[G[v][i]]){
            dfs(G[v][i]);
        }
    }
    vs.push_back(v);
}
void rdfs(int v,int k)
{
    used[v]=true;
    cmp[v]=k;
    for(int i=0;i<rG[v].size();i++){
        if(!used[rG[v][i]]){
            rdfs(rG[v][i],k);
        }
    }
}
void scc()
{
    memset(used,0,sizeof(used));
    vs.clear();
    for(int v=0;v<V;v++){
        if(!used[v]){
            dfs(v);
        }
    }
    memset(used,0,sizeof(used));
    int k=0;
    for(int i=vs.size()-1;i>=0;i--){
        if(!used[vs[i]]){
            rdfs(vs[i],k++);
        }
    }
    return;// k;
}
//模板
void solve()
{
    //0-N-1:x[i]
    //N-2N-1:~x[i]
    V=2*N;
    for(int i=0;i<N;i++){
        for(int j=0;j<i;j++){
            if(min(S[i]+D[i],S[j]+D[j])>max(S[i],S[j])){
                //x[i]--~x[j],x[j]--~x[i]
                add_edge(i,N+j);
                add_edge(j,N+i);
            }
            if(min(S[i]+D[i],T[j])>max(S[i],T[j]-D[j])){
                //x[i]--x[j],~x[j]--~x[i]
                add_edge(i,j);
                add_edge(N+j,N+i);
            }
            if(min(T[i],S[j]+D[j])>max(S[j],T[i]-D[i])){
                //~x[i]--~x[j],x[j]--x[i]
                add_edge(N+i,N+j);
                add_edge(j,i);
            }
            if(min(T[i],T[j])>max(T[i]-D[i],T[j]-D[j])){
                //~x[i]--x[j],~x[j]--x[i]
                add_edge(N+i,j);
                add_edge(N+j,i);
            }
        }
    }
    scc();
    //判断是否可满足
    for(int i=0;i<N;i++){
        if(cmp[i]==cmp[N+i]){
            printf("NO\n");
            return;
        }
    }
    //找到一组解
    printf("YES\n");
    for(int i=0;i<N;i++){
        if(cmp[i]>cmp[N+i]){
            //x[i]为真,在结婚仪式开始时
            printf("%02d:%02d %02d:%02d\n",S[i]/60,S[i]%60,(S[i]+D[i])/60,(S[i]+D[i])%60);
        }
        else{
            //x[i]为假,在结婚仪式结束时
            printf("%02d:%02d %02d:%02d\n",(T[i]-D[i])/60,(T[i]-D[i])%60,T[i]/60,T[i]%60);
        }
    }
}
int main()
{
    cin>>N;
    for(int i=0;i<N;i++){
        char s1[10],s2[10];
        cin>>s1>>s2;
        int a=10*(s1[0]-'0')+(s1[1]-'0');
        int b=10*(s1[3]-'0')+(s1[4]-'0');
        int c=10*(s2[0]-'0')+(s2[1]-'0');
        int d=10*(s2[3]-'0')+(s2[4]-'0');
        S[i]=60*a+b;
        T[i]=60*c+d;
        cin>>D[i];
    }
    solve();
    return 0;
}

3.LCA

1.定义

在有根树中,两个节点u和v的公共祖先中距离最近的那个被称为最近公共祖先(LCA,Lowest Common Ancestor)。

2.基础二分搜索的算法

记节点v到根的深度为depth(v),那么如果w是u和v的公共祖先的话,让u向上走|depth(u)-depth(w)|步,让v向上走|depth(v)-depth(w)|步,就都走到w。因此可以先让u和v较深的一方向上走|depth(u)-depth(v)|步,在一起一步一步向上走,就可以走到同一节点,可以在O(depth(u)+depth(v))时间内求出LCA。
vector<int> G[maxv];//邻接表表示
int root;//根节点

int parent[maxv];//父亲节点(根节点的父亲节点为-1)
int depth[maxv];//节点的深度

void dfs(int v,int p,int d)
{
    parent[v]=p;
    depth[v]=d;
    for(int i=0;i<G[v].size();i++){
        if(G[v][i]!=p){
            dfs(G[v][i],v,d+1);
        }
    }
}

//预处理
void init()
{
    //预处理出parent和depth
    dfs(root,-1,0);
}

//计算u和v的LCA
int lca(int u,int v)
{
    //让u和v走到同一深度
    while(depth[u]>depth[v]){
        u=parent[u];
    }
    while(depth[v]>depth[u]){
        v=parent[v];
    }

    //让u和v走到同一节点
    while(u!=v){
        u=parent[u];
        v=parent[v];
    }
    return u;
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值