图论:割点割边、强联通分量、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");
}
对于有实际情景的题目来说,关键就在于把握矛盾关系。之后套模板就行。POJ3683,POJ2723,POJ2749。在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;
}


浙公网安备 33010602011771号