专题1:图论
一、tarjan
我在之前的博客中详细介绍过,在这里直接放一个神奇的链接
首先了解强连通分量是什么,之后的就会很好理解
强连通分量
有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量。(来源:百度百科)
是不是根本没看懂,画几个图就好了,我们就以最经典的图(我看见了一堆关于tarjan的文章,都拿这个做样例,我也凑个热闹,毕竟我第一次接触时也用的这个)为例。
图片中的1,2,3,4在一起叫一个强联通分量
了解tarjan的运行过程
这个真的是费了巨长的时间,后来才发现整个过程都在进行一项工作,就是维护DFN数组和LOW数组。DFN[u]代表u的搜索次序编号(就是发现u的时间戳),LOW[u]表示的是u或u的子树能够追溯到的最早的栈中节点的次序号,那么我们从每一个细节开始演示tarjan的过程。
1.先去发现各个节点
和普通的搜索一样,就是从每一个未被发现的点开始进行搜索遍历,发现一个未被发现的点就直接把它进栈,然后接着更新该点的DFN值(你可能会问,那如果我发现了被发现过的点怎么办呢??别急,看下面)
2.更新LOW值
在发现被发现过的点之后就要更新自己的LOW值,因为LOW对应的是最小的值,并且栈中的一定是被发现过的(如果这个点被发现现过,而且被出栈过并且现在不在栈中,就证明这个点也无法再次被更新了),所以说更新的便是被发现的点的DFN值和自己的LOW值中小的那一个,也就是说这个更新的条件就是这个点在栈中。
3.回溯中的更新与出栈
回溯时要完成两个操作,一个是更新栈中所有点的LOW值,就是非常简单的回溯时把自己的LOW更新成下一点和自己中较小的那一个LOW值。第二个便是出栈,出栈的时候没什么好说的,遇见自己的DFN值和LOW值相等的就直接出栈,因为它们和别人也没什么关系,没和其他任何点构成强连通分量。
代码实现
void tarjan(int x)
{
exist[x]=1;
DFN[x]=LOW[x]=++tot;
stack[++index]=x;
for(int i=0;i<G[x].size();i++)
{
int v=edge[G[x][i]].to;
if(DFN[v]==0)
{
tarjan(v);
LOW[x]=min(LOW[x],LOW[v]);
}
else if(exist[v]==1)//这个就是是否在栈中的意思
{
LOW[x]=min(LOW[x],DFN[v]);
}
}
if(DFN[x]==LOW[x])
{
int k=0,t;
color++;//强连通分量序号
do//出栈
{
t=stack[index];
stack[index]=0;
index--;
a[t]=color;//每个结点的强连通分量序号
exist[t]=0;
k++;
}while(x!=t);
sum[color]=k;//每个强连通分量中节点的个数
}
}
二、2-SAT
2-SAT 问题是这样的:有
n
个布尔变量
对于这种问题,最常见的做法是拆点,把一个节点
i
拆分成2
对于“
xi
为假或者
xj
为假”这样的条件,我们连一条有向边2
i
+1
接下来注意考虑每个没有赋值的变量,先设定为假,然后逐步递归,寻找矛盾关系,如果矛盾,则假设为真,如果还有矛盾,那就证明该2-SAT 问题无解。
因为网上有很多tarjan版本的,所以我在这里给大家提供一种dfs版本的板子,同时也是HDU3062的题解
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
using namespace std;
bool mark[4005];
vector<int> G[4005];
int s[4005],c;
int n,m;
void Addedge(int x,int xval,int y,int yval)
{
x=(x-1)*2+xval;
y=(y-1)*2+yval;
G[x^1].push_back(y);
G[y^1].push_back(x);
}
bool dfs(int x)
{
if(mark[x^1]) return false;
if(mark[x]) return true;
mark[x]=true;
s[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
bool solve()
{
for(int i=0;i<n*2;i+=2)
{
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[s[--c]]=false;
if(!dfs(i+1)) return false;
}
}
}
return true;
}
int main()
{
freopen("hdu3062.in","r",stdin);
freopen("hdu3062.out","w",stdout);
while(~scanf("%d%d",&n,&m))
{
for(int i=1;i<=m;i++)
{
int a1,a2,c1,c2;
scanf("%d%d%d%d",&a1,&a2,&c1,&c2);
Addedge(a1,c1^1,a2,c2^1);
}
if(solve()) printf("YES\n");
else printf("NO\n");
for(int i=0;i<n*2;i++)
G[i].clear();
memset(mark,false,sizeof(mark));
}
return 0;
}
以上知识为集训时训练中的图论算法,其他的算法(如网络流等)不在这个专题中,会在以后详细介绍
下面推荐几道练习题,并附上题解
poj1236 Network of Schools
Tarjan缩点后对缩点后新图扫每个强连通块的入度出度
对于子任务A:求需要给多少个学校发软件。即为求有多少个入度为0的点。因为对于对于每个入度不为0的点一定可以从一个其他点走到.
对于子任务B:求入度为0的点个数与出度为0的点个数的最大值,需要添多少条边才能形成使所有点都可以被某个点到达,所以对于每个出度为0的点,需要拓展一条出边,对于每个入度为0的点,需要拓展一条入边,为了减少拓展边数,可以将所有出度为0的点拓展出边到某个入度为0的点的上,最后答案即为 max(入度为0点数,出度为0点数)。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <vector>
using namespace std;
struct apple
{
int u,v;
apple(int x,int y){u=x;v=y;}
};
int n,tot,index,color,ans1,ans2;;
const int inf=0x3f3f3f3f;
vector<apple> edge;
vector<apple> edge1;
vector<int> G[105];
vector<int> P[105];
int a[105],stack[105],DFN[105],LOW[105],in[105],out[105];
void Addedge(int u,int v)
{
edge.push_back(apple(u,v));
int m=edge.size();
G[u].push_back(m-1);
}
void Addedge1(int u,int v)
{
edge1.push_back(apple(u,v));
int m=edge1.size();
P[u].push_back(m-1);
}
int exist(int x)
{
for(int i=1;i<=index;i++)
{
if(stack[i]==x) return 1;
}
return 0;
}
void tarjan(int x)
{
DFN[x]=LOW[x]=++tot;
stack[++index]=x;
for(int i=0;i<G[x].size();i++)
{
int v=edge[G[x][i]].v;
if(DFN[v]==0)
{
tarjan(v);
LOW[x]=min(LOW[x],LOW[v]);
}
else if(exist(v)==1)
{
LOW[x]=min(LOW[x],DFN[v]);
}
}
if(DFN[x]==LOW[x])
{
int t;
color++;
do
{
t=stack[index];
stack[index]=0;
index--;
a[t]=color;
}while(t!=x);
}
}
int main()
{
freopen("network.in","r",stdin);
freopen("network.out","w",stdout);
scanf("%d",&n);
for(int u=1;u<=n;u++)
{
int v;
while(scanf("%d",&v) && v!=0)
Addedge(u,v);
}
for(int i=1;i<=n;i++)
if(DFN[i]==0) tarjan(i);
for(int i=0;i<edge.size();i++)
if(a[edge[i].u]!=a[edge[i].v])
Addedge1(a[edge[i].u],a[edge[i].v]);
for(int i=0;i<edge1.size();i++)
{
int u=edge1[i].u,v=edge1[i].v;
out[u]++;in[v]++;
}
for(int i=1;i<=color;i++)
{
if(!in[i]) ans1++;
if(!out[i]) ans2++;
}
printf("%d\n",ans1);
if(color!=1) printf("%d\n",max(ans1,ans2));
else printf("0\n");
return 0;
}
bzoj1823 满汉全席
2-sat 模板题,感觉和HDU3062无任何区别,翻译一下题意就可以发现了,每个材料可以做成满菜和汉菜,就相当于一真一假。处理方式同HDU3062
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
using namespace std;
bool mark[40005];
vector<int> G[40005];
int s[40005],c;
int t,n,m;
char getc()
{
char ch=getchar();
while(ch>'z'||ch<'a')ch=getchar();
return ch;
}
void Addedge(int x,int xval,int y,int yval)
{
x=(x-1)*2+xval;
y=(y-1)*2+yval;
G[x^1].push_back(y);
G[y^1].push_back(x);
}
bool dfs(int x)
{
if(mark[x^1]) return false;
if(mark[x]) return true;
mark[x]=true;
s[c++]=x;
for(int i=0;i<G[x].size();i++)
if(!dfs(G[x][i])) return false;
return true;
}
bool solve()
{
for(int i=0;i<n*2;i+=2)
{
if(!mark[i] && !mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0) mark[s[--c]]=false;
if(!dfs(i+1)) return false;
}
}
}return true;
}
int main()
{
freopen("bzoj1823.in","r",stdin);
freopen("bzoj1823.out","w",stdout);
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
char c1,c2;
int tmp1,tmp2,a1,a2;
c1=getc();
scanf("%d",&a1);
c2=getc();
scanf("%d",&a2);
if(c1=='m') tmp1=1;else tmp1=0;
if(c2=='m') tmp2=1;else tmp2=0;
Addedge(a1,tmp1,a2,tmp2);
}
if(solve()) printf("GOOD\n");
else printf("BAD\n");
for(int i=0;i<n*2;i++)
G[i].clear();
memset(mark,false,sizeof(mark));
}
return 0;
}
poj3170
暴搜,从起点搜索每个woods,从骑士搜索每个woods,然后枚举每个woods,输出答案
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <queue>
using namespace std;
struct dot
{
int x,y,val;
};
int w,h,ans=0x7fffffff;
int map[1005][1005],vist[1005][1005];
int sx,sy,ex,ey;
int b[1005][1005],k[1005][1005];
int dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
void bfsb()
{
queue<dot> q;
dot tmp;
tmp.x=sx;tmp.y=sy;tmp.val=0;
q.push(tmp);
while(!q.empty())
{
dot u=q.front();q.pop();
int x=u.x,y=u.y,val=u.val;
for(int i=0;i<4;i++)
{
int newx=x+dx[i],newy=y+dy[i];
if(newx>=1 && newx<=h && newy>=1 && newy<=w && map[newx][newy]!=1 && !vist[newx][newy])
{
tmp.x=newx;tmp.y=newy;tmp.val=val+1;
vist[newx][newy]=1;
if(map[newx][newy]==4) b[newx][newy]=val+1;
q.push(tmp);
}
}
}
}
void bfsk()
{
queue<dot> q;
dot tmp;
tmp.x=ex;tmp.y=ey;tmp.val=0;
q.push(tmp);
while(!q.empty())
{
dot u=q.front();q.pop();
int x=u.x,y=u.y,val=u.val;
for(int i=0;i<4;i++)
{
int newx=x+dx[i],newy=y+dy[i];
if(newx>=1 && newx<=h && newy>=1 && newy<=w && map[newx][newy]!=1 && !vist[newx][newy])
{
tmp.x=newx;tmp.y=newy;tmp.val=val+1;
vist[newx][newy]=1;
if(map[newx][newy]==4) k[newx][newy]=min(k[newx][newy],val+1);
q.push(tmp);
}
}
}
}
int main()
{
freopen("bzoj1671.in","r",stdin);
freopen("bzoj1761.out","w",stdout);
scanf("%d%d",&w,&h);
for(int i=1;i<=h;i++)
for(int j=1;j<=w;j++)
{
scanf("%d",&map[i][j]);
if(map[i][j]==2) sx=i,sy=j;
if(map[i][j]==3) ex=i,ey=j;
}
memset(b,63,sizeof(b));
memset(k,63,sizeof(k));
bfsb();
memset(vist,0,sizeof(vist));
bfsk();
for(int i=1;i<=h;i++)
for(int j=1;j<=w;j++)
if(map[i][j]==4) ans=min(ans,b[i][j]+k[i][j]);
printf("%d",ans);
return 0;
}