4-26 -> 4-27
强连通分量的概念上篇已经讲得很详细了,今天做题我给模板上加了一个数组 belong , belong[i] 表示点i 存在于哪一个强连通分量之中
//
先来看第一道题:
hdu3072 : http://acm.hdu.edu.cn/showproblem.php?pid=3072
题意弯弯绕绕的, 从0 这个点传送信息到每一个点, 意味着 0 这点可到达每一个其他点,每一条边有一定的费用,但是在强连通分量里面所有边是没有费用的,求完成这个连通工程的最小的费用
2016-5-10回顾:
T1:其实只要牢记一点,强连通分量最重要的点之一就是 每一个分量的出边和入边
: 一开始想歪了,题意也没有读清楚,重点是0 这个点要能 到达所有其他点, 首先我们可以跑一遍tarjan,把图中的所有强连通分量都跑出来,这些点不管相互有多少费用都变为0,然后将这个强连通分量看为一个点, 我们需要得到最小的权值和。 其实很简单,但是我自己还没有那个思维, 记住一点:对于这种强连通分量,他的入边,出边是很重要的: 比如,对于这一道题,我们只要找出每一个强连通分量的最小入边(起点是另外一个分量),然后除了0 这一点的强连通分量,我们把其他每一个强连通的最小入边加起来即可:
#include <cstdio>
#include <cmath>
#include <cstring>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
#include <sstream>
#include <queue>
#include <typeinfo>
#include <fstream>
#include <map>
#include <stack>
using namespace std;
#define M 50005 //题目中可能的最大点数
int STACK[M],top; //Tarjan 算法中的栈
bool InStack[M]; //检查是否在栈中
int DFN[M]; //深度优先搜索访问次序
int Low[M]; //能追溯到的最早的次序
int Comnumber; //有向图强连通分量个数
int Index; //索引号
vector <int > Edge[M]; //邻接表表示
vector <int> Com[M]; //获得强连通分量结果
int belong[M]; //给每一个标记他的强连通分量编号
int in[M]; //记录每一个连通分量的入边
int sta[100005],to[100005],c[100005]; //记录每一条边,然后可以用On 找出每一个强连通分量的最小入边,或出边
void Tarjan(int i)
{
int j;
DFN[i]=Low[i]=Index++;
InStack[i]=true;
STACK[++top]=i;
for (int e=0;e<Edge[i].size();e++){
j=Edge[i][e];
if (DFN[j]==-1){ //没有访问过
Tarjan(j);
Low[i]=min(Low[i],Low[j]);
}
else if (InStack[j])
Low[i]=min(Low[i],DFN[j]); //这一层的递归里面。Low[j]是还没有更新的,讲道理这里写DFN和LOW j没区别
}
if (DFN[i]==Low[i])
{
Comnumber++;
do
{
j=STACK[top--];
InStack[j]=false;
belong[j]=Comnumber;
}
while (j!=i);
}
}
void solve(int N) //此图中点的个数,注意是0-indexed!
{
Comnumber=0;
top=0;
Index=1;
memset(STACK,-1,sizeof(STACK));
memset(InStack,0,sizeof(InStack));
memset(DFN,-1,sizeof(DFN));
memset(Low,-1,sizeof(Low));
for(int i=1;i<=N;i++)
if(DFN[i]==-1)
Tarjan(i);
}
//想法:
//先对强连通分量进行缩点统计点个数
int main(){
int n,m;
//freopen("1.txt","r",stdin);
while(~scanf("%d %d",&n,&m)){
for(int i=1;i<=M;i++){
Edge[i].clear();
}
for(int i=1;i<=m;i++){
scanf("%d %d %d",&sta[i],&to[i],&c[i]);
sta[i]++;
to[i]++;
Edge[sta[i]].push_back(to[i]);
}
solve(n);
for(int i=1;i<=Comnumber;i++){ //改动
in[i]=100001;
}
for(int i=1;i<=m;i++){
int u=sta[i],v=to[i],C=c[i];
if(belong[u]!=belong[v]){
in[belong[v]]=min(in[belong[v]],C);
}
}
int ans=0;
for(int i=1;i<=Comnumber && i!=belong[1] ;i++){
if(in[i]!=100001)
ans+=in[i];
}
printf("%d\n",ans);
}
return 0;
}
第二道题:
hdu 4635
http://acm.hdu.edu.cn/showproblem.php?pid=4635
题意:
题意简洁:对于一个有向简单图(没重边,没自环),问最多可以加多少边 使得整个图任然是一个简单有向图,但是整个图不是强连通的!
2016-5-10回顾:
T2:第一步对已经是强连通分量的块 缩为一个点(并且可以加一些边),对于剩下的图必然存在一个入度为0 的“点”或者出度为0 ,否则必然是强连通啊, 然后这个点不动,其他点组成完全图,应该就是最多的边吧, 讲道理这样写也不复杂很快可以A吧。 还是年轻
哦哦,看完正解发现自己还是太年轻了,忽略了一个地方:“对于我们隔离的点”必须是包含点数最少的块,是吧,因为这样在整个的完全图里面这样删除的边就是最少的,然后还要考虑初始给定的边是不能够删除的,所以应该找 “初始图中”的 “出度为0或者入度为0” 且“ 包含点最少” 的强连通分块,理论上
这样就是正解了
这个题能保留对于这些题的想法其实是很简单的: 对于一开始给的图(一开始给了m条边),肯定包含一些强连通分量,我们可以把强连通分量看成点。
然后对于这个图,我们如果要保持整体不是强连通,我们就只需要排除一个 点(强连通分量) 即可: 即这个点没有 入边,或者没有出边,满足一个条件 整个图就不是强连通图了。
如果有n个点 ,那么对于完全图 就有 n*(n-1)条边,
一开始给了m条边,所以ans=n*(n-1) -m;
那么我们如何找强连通分量呢? 自然我们找到那个包含 点最少的强连通分量,把他的入边 或者 出边都删掉即可以(这两个其实边数是相等的)。
所以ans -= v*(n-v);
仔细想想这样真的对吗? 是不是想错的地方,在这个地方wa了一发, 没错 我们没有意识到一开始给的m条边有可能对于 这个包含最小点的分量 既有入边也有出边,这个时候这个强连通分量是无论如何都删不掉的。
所以我们必须找一开始给出的边中 入边或者 出边为0的强连通分量中 包含点最小的 分量。
#include <cstdio>
#include <cmath>
#include <cstring>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
#include <sstream>
#include <queue>
#include <typeinfo>
#include <fstream>
#include <map>
#include <stack>
using namespace std;
#define M 100005 //题目中可能的最大点数
int STACK[M],top; //Tarjan 算法中的栈
bool InStack[M]; //检查是否在栈中
int DFN[M]; //深度优先搜索访问次序
int Low[M]; //能追溯到的最早的次序
int Comnumber; //有向图强连通分量个数
int Index; //索引号
vector <int > Edge[M]; //邻接表表示
vector <int > Com[M]; //获得强连通分量结果
int in[M];
int out[M];
int num[M]; //给每一个标记他的强连通分量编号 = belong
void Tarjan(int i)
{
int j;
DFN[i]=Low[i]=Index++;
InStack[i]=true;
STACK[++top]=i;
for (int e=0;e<Edge[i].size();e++){
j=Edge[i][e];
if (DFN[j]==-1){ //没有访问过
Tarjan(j);
Low[i]=min(Low[i],Low[j]);
}
else if (InStack[j])
Low[i]=min(Low[i],DFN[j]); //这一层的递归里面。Low[j]是还没有更新的,讲道理这里写DFN和LOW j没区别
}
if (DFN[i]==Low[i])
{
//cout<<"TT "<<i<<" "<<Low[i]<<endl;
Comnumber++;
do
{
j=STACK[top--];
InStack[j]=false;
num[j]=Comnumber;
Com[Comnumber].push_back(j);
}
while (j!=i);
}
}
void solve(int N) //此图中点的个数,注意是0-indexed!
{
Comnumber=0;
top=0;
Index=1;
memset(STACK,-1,sizeof(STACK));
memset(InStack,0,sizeof(InStack));
memset(DFN,-1,sizeof(DFN));
memset(Low,-1,sizeof(Low));
memset(num,-1,sizeof(num));
memset(out,0,sizeof(out));
memset(in,0,sizeof(in));
for(int i=1;i<=N;i++)
if(DFN[i]==-1)
Tarjan(i);
}
int main(){
int t;
int n,m;
//freopen("1.txt","r",stdin);
scanf("%d",&t);
for(int k=1;k<=t;k++){
for(int i=1;i<=M;i++)
Edge[i].clear();
for(int i=1;i<=M;i++)
Com[i].clear();
printf("Case %d: ",k);
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++){
int sta,to;
cin>>sta>>to;
Edge[sta].push_back(to);
}
solve(n);
// printf("Comnumber=%d\n",Comnumber);
if(Comnumber==1){
printf("-1\n");
continue;
}
__int64 minn=100005;
for(int i=1;i<=n;i++)
for(int j=0;j<Edge[i].size();j++)
if(num[i]!=num[Edge[i][j]]) //如果这两点 不是在同一个连通分量里面
{
out[num[i]]++;
in[num[Edge[i][j]]]++;
}
for(int i=1;i<=Comnumber;i++)
{
if(in[i]==0||out[i]==0)
{
if( (__int64)Com[i].size() < minn)
minn=(__int64)Com[i].size();
}
}
__int64 ans=(__int64)n*(n-1);
ans-=(__int64)m;
ans-=(__int64)( (n-minn)*minn);
printf("%I64d\n",ans);
}
return 0;
}
poj 2366
这个问题包含两个很经典的问题:
1.对于一个有向图 , 最少需要发几份文件,才能让文件传遍每一个点。
就是找入度为0 的连通块有几个,有几个就发几份
2.对于一个有向图,最少加几条边让他成为一个强连通图(彼此可达)
第二个问题解法:
如果这个图本身是一个强连通图,那么不要加。
否则,添加边是 max(入度为0 的块 数量, 出度为 0 的块的数量) 。 为什么取最大值就是需要加的边呢?
在DAG上要加几条边,才能使得DAG变成强连通的,问题2的答案就是多少
加边的方法:
要为每个入度为0的点添加入边,为每个出度为0的点添加出边
假定有 n 个入度为0的点,m个出度为0的点,如何加边?
把所有入度为0的点编号 0,1,2,3,4 ….N -1
每次为一个编号为i的入度0点可达的出度0点,添加一条出边,连到编号为(i+1)%N 的那个出度0点,
这需要加n条边
若 m <= n,则
加了这n条边后,已经没有入度0点,则问题解决,一共加了n条边
若 m > n,则还有m-n个入度0点,则从这些点以外任取一点,和这些点都连上边,即可,这还需加m-n条边。
所以,max(m,n)就是第二个问题的解
#include <cstdio>
#include <cmath>
#include <cstring>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <set>
#include <vector>
#include <sstream>
#include <queue>
#include <typeinfo>
#include <fstream>
#include <map>
#include <stack>
using namespace std;
#define M 110 //题目中可能的最大点数
int STACK[M],top; //Tarjan 算法中的栈
bool InStack[M]; //检查是否在栈中
int DFN[M]; //深度优先搜索访问次序
int Low[M]; //能追溯到的最早的次序
int Comnumber; //有向图强连通分量个数
int Index=0; //索引号
int belong[M]; //给每一个标记他的强连通分量编号
vector<int> Edge[M]; //邻接表表示
vector<int> Com[M]; //获得强连通分量结果
int in[M];
int out[M]; //存每一个强连通分量的出度,入度
void Tarjan(int i)
{
int j;
DFN[i]=Low[i]=Index++;
InStack[i]=true;
STACK[++top]=i;
for (int e=0;e<Edge[i].size();e++){
j=Edge[i][e];
if (DFN[j]==-1){
Tarjan(j);
Low[i]=min(Low[i],Low[j]);
}
else if (InStack[j])
Low[i]=min(Low[i],DFN[j]); //这一层的递归里面。Low[j]是还没有更新的,讲道理这里写DFN和LOW j没区别
}
if (DFN[i]==Low[i])
{
//cout<<"TT "<<i<<" "<<Low[i]<<endl;
Comnumber++;
do
{
j=STACK[top--];
InStack[j]=false;
Com[Comnumber].push_back(j);
belong[j]=Comnumber;
}
while (j!=i);
}
}
void solve(int N) //此图中点的个数,注意是0-indexed!
{
Comnumber=0;
top=0;
Index=1;
memset(STACK,-1,sizeof(STACK));
memset(InStack,0,sizeof(InStack));
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
memset(DFN,-1,sizeof(DFN));
memset(Low,-1,sizeof(Low));
for(int i=1;i<=N;i++)
if(DFN[i]==-1)
Tarjan(i);
}
void init(){
for(int i=1;i<=M;i++){
Edge[i].clear();
Com[i].clear();
}
}
/* 此算法正常工作的基础是图是1-indexed的。 */
int main(){
int n;
freopen("1.txt","r",stdin);
while(~scanf("%d",&n)){
init();
for(int i=1;i<=n;i++){
int tem;
while(~scanf("%d",&tem) &&tem){
Edge[i].push_back(tem);
}
}
solve(n);
for(int i=1;i<=n;i++){
int len=(int)Edge[i].size();
for(int j=0;j<len;j++){
int u=i,v=Edge[i][j];
if(belong[u] != belong[v]){ //妈蛋 ,居然忘记写这个
in[ belong[v] ]++;
out[belong[u] ]++;
}
}
}
int ans1=0;
int ans2=0;
if(Comnumber==1){
printf("1\n0\n");
continue;
}
for(int i=1;i<=Comnumber;i++){
if(in[i]==0){
ans1++;
}
if(out[i]==0)
ans2++;
}
ans2=max(ans1,ans2);
printf("%d\n",ans1);
printf("%d\n",ans2);
}
}