图论专题班连通性例题和练习

2022牛客 图论 连通性(强连通、割点和桥)

概念

双连通分量

如果一个图不存在割点,则它是一个点双连通图,一个图的极大点双连通子图是他的点双连通分量。

  • 任何两个割点之间的路径不一定是桥。
  • 桥的两个端点不一定是割点。

强连通分量

在有向图 G G G中,如果两个顶点 u , v u,v u间有一条从 u u v v 的有向路径,同时还有一条从 v v u u 的有向路径,则称两个顶点强连通。如果有向图 G G G的每两个顶点都强连通,称 G G G是一个强连通图。有向非强连通图的极大强连通子图,称为强连通分量。

tarjan

时间戳 d f n dfn dfn:第几个搜到这个点
返祖边:搜索树上一个点连向其祖先节点的边
追溯值 l o w low low:当前点及其子树的反祖边能够到达的dfn的最小值。
如果 < u , v > <u,v> <u,v>是搜索树的边 : l o w [ u ] = m i n ( l o w [ u ] , l o w [ v ] ) low[u] = min(low[u],low[v]) low[u]=min(low[u],low[v])
如果 < u , v > <u,v> <u,v>是反组边: l o w [ u ] = m i n ( l o w [ u ] , d f n [ v ] ) low[u] = min(low[u],dfn[v]) low[u]=min(low[u],dfn[v])

割点

1. u u u是割点 , v v v u u u 搜索树上的一个儿子 : d f n [ u ] < = l o w [ v ] dfn[u] <= low[v] dfn[u]<=low[v] ( v v v的子树中没有反祖边能够跨越 u u u点,即到达u以上的点)
2.有多个儿子的根节点

割边(桥)

搜索树上 v v v u u u的儿子: d f n [ u ] < l o w [ v ] dfn[u] < low[v] dfn[u]<low[v] ( v v v的子树中没有返祖边能够跨越 < u , v > <u,v> <u,v>这条边)

tarjan求割点,桥

割点求:

//割点的判断
void tarjan(int x){
	dfn[x] = low[x] = ++cnt;
	int son = 0;
	for(int i  = h[u];~i;i=ne[i]{
		int y = e[i];
		//这里的if else 用到上面(tarjan)low的更新条件(返祖边,搜素边)
		if(!dfn[y]){
			tarjan(y);
			son ++;//有多个儿子的根节点的判断。
			low[x] = min(low[x],low[y]);
			if(low[y] >= dfn[x]){//割点判断条件
				if(x != root || son > 1)cut[x] = 1;
			}
		}else low[x] = min(low[x],dfn[y]);
	}
}

tips : 课程 对else条件后的是 使用 ① l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x] = min(low[x],dfn[y]) low[x]=min(low[x],dfn[y]) 和 ② l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x] = min(low[x],low[y]) low[x]=min(low[x],low[y]) 举了反例。
使用②,会造成跳点 , x − y − z x-y-z xyz 运行的时候 z z z会跳到 x x x实际上应该为 y y y,在求割点和桥的时候会造成影响,对强连通分量无影响。

割边求:

//对于求割边去掉等号,标记改一下

void tarjan(int u,int from){    
    dfn[u] = low[u] = ++tim;
    stk[++top] = u;
    for(int i = h[u];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y,i);
            low[u] = min(low[u],low[y]);
            //桥
            if(dfn[u] < low[y])
               is_bridge[i] = is_bridge[i^1] = true;
        }
        //i不是返回u的那条反向边
        else if(i != (from ^ 1)) low[u] = min(dfn[y],low[u]);
    }
}

点双联通分量 v-DCC

点双连通图 的一个定义是:

图中任意两不同点之间都有至少两条点不重复的路径。(或 极大的,不存在割点的图。)

  • 每个割点至少属于两个连通分量

求法

  • 维护一个栈
  • 第一次访问某个节点时将其入栈
  • 当割点判断法则中 d f n [ x ] < = l o w [ y ] dfn[x] <= low[y] dfn[x]<=low[y] 成立时,不断从栈中弹出节点,直到 y y y被弹出,这些被弹出的点和 x x x一起构成一个点双连通分量。

缩点建图方法:

每个割点割当前点连通分量连一条边。

//补图

代码

void Tarjan(int u) {
  low[u] = dfn[u] = ++tim;                // low 初始化为当前节点 dfn
  stk[++top] = u;                          // 加入栈中
  for (int i = h[u];~i;i=ne[i]) {                    // 遍历 u 的相邻节点
    int v = e[i];
    if (!dfn[v]) {                        // 如果未访问过
      Tarjan(v);                          // 递归
      low[u] = min(low[u], low[v]);  // 未访问的和 low 取 min
      if (low[v] == dfn[u]) {  // 标志着找到一个以 u 为根的点双连通分量
        ++cnt; 
        int y;
        do{
			y = stk[top--];
			dcc[cnt].push_back(y);        
		} while(y != v); 
        dcc[cnt].push_back(u);
      }
    } else
      low[u] = min(low[u], dfn[v]);  // 已访问的和 dfn 取 min
  }
}

边双联通分量 e - DCC

边双连通:

极大的,不含桥的连通区域,(删掉一条边之后,图仍联通。)
存在两条边不相交(两条路径没重边)的路径

步骤:

  1. 确定桥 ( ( u , v ) = > d f n [ u ] < l o w [ v ] ) ((u,v) => dfn[u] < low[v]) ((u,v)=>dfn[u]<low[v])
  2. (第一种:)将所有桥删掉(标记桥,dfs所有点)
    (第二种:)stack ( d f s [ x ] = = l o w [ x ] dfs[x] == low[x] dfs[x]==low[x])

代码

//from:到u的这条边
void tarjan(int u,int from){    
    dfn[u] = low[u] = ++tim;
    stk[++top] = u;
    for(int i = h[u];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y,i);
            low[u] = min(low[u],low[y]);
            //桥
            if(dfn[u] < low[y])
               is_bridge[i] = is_bridge[i^1] = true;
        }
        //i不是返回u的那条反向边
        else if(i != (from ^ 1)) low[u] = min(dfn[y],low[u]);
    }
    //边联通分量 缩点
    if(low[u] == dfn[u]){
        int y;
        cnt++;
        do{
            y = stk[top--];
            dcc[y] = cnt;
        }while(y!=u); 
    }
}

强连通分量

有向图
tarjan求强连通分量

  • dfn[n] 时间戳
  • low[n] 他和他的子树里返祖边和横向边能够连到还没出栈的dfn最小点
  • 在dfs的时候维护一个栈第一次访问某个点时将其加入栈中,当一个点x的dfn[x] == low[x] ,他就是这个强连通分量里面搜索的树中最高的点,将栈里点出栈直到x也出栈为止,这些点就组成一个强连通分量。
void tarjan(int x){
	dfn[x] = low[x] = ++Cnt;
	stack[++top] = x;
	st[x] = true;
	for(int i=h[x];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y);
			low[x] = min(low[x],low[y]);
		}else if(st[y]) low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x]){
		scc ++;
		int y;
		do{
			 y = stack[top--];
			st[y] = false;
			num[y] = scc;
		}while(y != x);
	}
}

扩展 · 圆方树

oi wiki

特点:

把一般的图转化为树

圆方树定义:

在圆方树中,原来的每个点对应一个 圆点,每一个点双(点双联通分量)对应一个方点。
所以共有 n + c n+c n+c个点,其中 n n n 是原图点数, c c c是原图点双连通分量的个数。

例图:
在这里插入图片描述
建图(求解双联通分量):

//oi wiki 源码(改了一下):
void Tarjan(int u) {
  low[u] = dfn[u] = ++tim;              
  stk[++top] = u;                          
  for (int i = h[u];~i;i=ne[i]) {
  	int v = e[i];                    
    if (!dfn[v]) {                        
      Tarjan(v);                        
      low[u] = min(low[u], low[v]); 
      if (low[v] == dfn[u]) {  // 标志着找到一个以 u 为根的点双连通分量
        ++cnt;                 // 增加方点个数
        // 将点双中除了 u 的点退栈,并在圆方树中连边
        int y;
        do{
			y = stk[top--];
			bcc[cnt].push_back(y);
			bcc[y].push_back(cnt);
		}while(y != v);
        // 注意 u 自身也要连边(但不退栈)
        bcc[cnt].push_back(u);
        bcc[u].push_back(cnt);
      }
    } else
      low[u] = std::min(low[u], dfn[v]);  // 已访问的和 dfn 取 min
  }
}

性质:

  • 这个新的树上面,度大于1的圆点,就是割点

扩展 · 2-sat

oi wiki
由对称性解2-SAT问题 by 伍昱

定义:

给出 n n n 个集合,每个集合有个元素,已知若干个 ( a , b ) (a,b) (a,b),表示 a a a b b b 矛盾(其中 a a a b b b 属于不同的集合)。然后从每个集合选择个元素,判断能否一共选 n n n 个两两不矛盾的元素。

步骤:

  • 假设有 a 1 , a 2 a1,a2 a1,a2 b 1 , b 2 b1,b2 b1,b2 两对,已知 a 1 a1 a1 b 2 b2 b2 间有矛盾,为了方案合适,由于两者中必须选一个,所以我们就要拉两条有向边 ( a 1 , b 1 ) (a1,b1) (a1,b1) ( b 2 , a 2 ) (b2,a2) (b2,a2) 表示选了 a 1 a1 a1 则必须选 b 1 b1 b1,选了 b 2 b2 b2 则必须选 a 2 a2 a2 才能够自洽。
  • 然后通过这样子建边我们跑一遍 T a r j a n S C C Tarjan SCC TarjanSCC 判断是否有一个集合中的两个元素在同一个 S C C SCC SCC 中,若有则输出不可能,否则输出方案。构造方案只需要把几个不矛盾的 SCC 拼起来就好了。

输出问题:

如果选择(连通分量) S i Si Si,那么对于所有 S i → S j Si→Sj SiSj S j Sj Sj 都必须被选择。 而 S i ′ Si' Si 必定不可选,这样 S i ’ Si’ Si的所有前代节点也必定不可选(将这一过程称之为删除)。
由推广2(推广2:对于任意一对 S i : ( a i , b i ) 所 属 的 环 , S i ′ : ( a i ′ , b i ′ ) 所 属 的 环 Si :(ai,bi)所属的环 , Si' : (ai',bi')所属的环 Si(ai,bi),Si(ai,bi) S i Si Si的后代节点与 S i ′ Si' Si 的前代节点相互对称( : 建图的时候是 a i − > b i , b i ′ − > a i ′ ai -> bi, bi' -> ai' ai>bi,bi>ai)。)可以得到,这样的删除不会导致矛盾。

假设选择S3’
→选择S3’的后代节点, S1’
→删除S3
→删除S3的前代节点S1
S1与S1’是对称的

另外,若每次盲目的去找一个未被确定的Si,时间复杂度相当高。
以自底向上的顺序进行选择、删除,这样还可以免去“选择Si的后代节点”这一步。
用拓扑排序实现自底向上的顺序 ( t a r j a n tarjan tarjan的强连通分量编号和拓扑序相反)。
总结 : 选强连通分量大的那个表示的含义。

例题:P4782 【模板】2-SAT 问题

例题

可达性 (tips:强连通分量)

给出一个 0 ≤ N ≤ 105 点数、0 ≤ M ≤ 105 边数的有向图,
输出一个尽可能小的点集,使得从这些点出发能够到达任意一点,如果有多个这样的集合,输出这些集合升序排序后字典序最小的。

输入描述:
第一行为两个整数 1 ≤ n, m ≤ 105,
接下来 M 行,每行两个整数 1 ≤ u, v ≤ 105 表示从点 u 至点 v 有一条有向边。
数据保证没有重边、自环。

输出描述:
第一行输出一个整数 z,表示作为答案的点集的大小;
第二行输出 z 个整数,升序排序,表示作为答案的点集。

思路:

缩点,入度为0的点。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
//526526
using namespace std;
const int N =1e5+10;
int n ,m;
int stack[N],top;
int cnt,dfn[N],low[N];
vector<int> v;
bool st[N];
int in[N];
int scc[N],tim;
int ans[N];
int h[N],ne[N],e[N],idx;
void add(int a,int b){
	ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
void tarjan(int x){
	dfn[x] = low[x] = ++tim;
	stack[++top] = x;
	st[x] = true;
	for(int i = h[x];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y);
			low[x] = min(low[x],low[y]);
		}else if(st[y])low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x]){
		int y;
		++cnt;
		ans[cnt] = N+1;
		do{
			y = stack[top--];
			st[y] = false;
			scc[y] = cnt;
			ans[cnt] = min(ans[cnt],y);
		}while(x != y);	
	}
}
int main(){
	cin >> n >> m;
	int x,y;
	memset(h,-1,sizeof h);
	for(int i = 1;i<=m;i++){
		cin  >> x>> y;
		add(x,y);		
	}
	for(int i = 1;i<=n;i++){
		if(!dfn[i])tarjan(i);
	}
	for(int i = 1;i<=n;i++){
		for(int j = h[i];~j;j=ne[j]){
			int y = e[j];
			if(scc[y]!=scc[i])in[scc[y]] ++;
		}
	}
	for(int i = 1;i<=cnt;i++){
		if(in[i] == 0)v.push_back(ans[i]);
	}
	sort(v.begin(),v.end());
	cout << v.size() << endl;
	for(int i = 0;i<v.size();i++)cout << v[i] << " ";
	return 0;
}

[HAOI2006]受欢迎的牛 (tips : 强连通分量)

每一头牛的愿望就是变成一头最受欢迎的牛。现在有N头牛,给你M对整数(A,B),表示牛A认为牛B受欢迎。
这种关系是具有传递性的,如果A认为B受欢迎,B认为C受欢迎,那么牛A也认为牛C受欢迎。
你的任务是求出有多少头牛被所有的牛认为是受欢迎的。

输入描述:
第一行两个数N,M。
接下来M行,每行两个数A,B,意思是A认为B是受欢迎的(给出的信息有可能重复,即有可能出现多个A,B)

输出描述:
一个数,即有多少头牛被所有的牛认为是受欢迎的。

思路:
缩点,找出度为1的点,一个答案为这个缩点的大小,多个无解。

#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e4+10,M = 5e4+10;
int h[N],e[M],ne[M],idx;
int tim,dfn[N],low[N];
int stack[N],top;
int scc[N],cnt;
bool st[N];
int out[N];
int siz[N];
void add(int a,int b){
    ne[idx] = h[a] ,  e[idx] = b,h[a] = idx++;
}
int n,m;
void tarjan(int x){
    dfn[x] = low[x] = ++tim;
    stack[++top] = x;
    st[x] = true;
    for(int i = h[x];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y);
            low[x] = min(low[x],low[y]);
        }else if(st[y]) low[x] = min(low[x],dfn[y]);
    }
    if(dfn[x] == low[x]){
        int y;
        cnt++;
        do{
            y = stack[top--];
            st[y] = false;
            scc[y] = cnt;
            siz[cnt] ++;
        }while(y != x);
    }
}

int main(){
    cin >> n >> m;
    memset(h,-1,sizeof h);
    for(int i = 1;i<=m;i++){
        int x,y;
        cin >> x >> y;
        add(x,y);
    }
    for(int i = 1;i<=n;i++){
        if(!dfn[i])tarjan(i);
    }
    for(int i = 1;i<=n;i++){
        for(int j = h[i];~j;j=ne[j]){
            int y = e[j];
            if(scc[y] != scc[i])out[scc[i]] ++;
        }
    }
    int ans = 0;
    for(int i = 1;i<=cnt;i++){
        if(out[i] == 0){
            if(ans != 0){
                ans = 0;
                break;
            }
            ans = siz[i];
        }
    }
    cout << ans << endl;
    return 0;
}

[ZJOI2007]最大半连通子图 (tips : 强联通分量)

一个有向图G=(V,E)称为半连通的(Semi-Connected),如果满足:u,v∈V,满足u→v或v→u,即对于图中任意 两点u,v,存在一条u到v的有向路径或者从v到u的有向路径。
若G’=(V’,E’)满足V’?V,E’是E中所有跟V’有关的边, 则称G’是G的一个导出子图。
若G’是G的导出子图,且G’半连通,则称G’为G的半连通子图。
若G’是G所有半连通子图 中包含节点数最多的,则称G’是G的最大半连通子图。
给定一个有向图G,请求出G的最大半连通子图拥有的节点数K ,以及不同的最大半连通子图的数目C。由于C可能比较大,仅要求输出C对X的余数。

输入描述:
第一行包含两个整数N,M,X。N,M分别表示图G的点数与边数,X的意义如上文所述
接下来M行,每行两个正整数a, b,表示一条有向边(a, b)。
图中的每个点将编号为1,2,3…N,保证输入中同一个(a,b)不会出现两次。
N ≤ 100000, M ≤ 1000000;对于100%的数据, X ≤ 10^8

输出描述:
应包含两行,第一行包含一个整数K。第二行包含整数C Mod X.

思路:

缩点,建立新图(注意重边),topu+dp维护结果。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<stack>
#include<map>
//526526
using namespace std;
typedef pair<int,int> PII;
const int N = 1e5+10,M = 1e6+10;
int h[N],e[M],ne[M],idx;
int h2[N],e2[M],ne2[M],idx2;
int Stack[N],top;
int cnt,scc[N];
int tim,dfn[N],low[N];
bool st[N];
int in[N],out[N];
int siz[N],f[N];
long long ans[N];
int n,m,p;
map<PII,int>  mp;
void add(int a,int b){
	ne[idx] = h[a] , e[idx] = b,h[a] = idx++;
}
void add2(int a,int b){
	ne2[idx2] = h2[a] , e2[idx2] = b,h2[a] = idx2++;
}
void tarjan(int x){
	dfn[x] = low[x] = ++tim;
	Stack[++top] = x;
	st[x] = true;
	for(int i = h[x];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y);
			low[x] = min(low[x],low[y]);
		}else if(st[y])low[x] = min(low[x],dfn[y]);
	}
	if(dfn[x] == low[x]){
		int y ;
		cnt++;
		do{
			y = Stack[top--];
			st[y] = false;
			scc[y] = cnt;
			siz[cnt] ++;
		}while(x!=y);
	}
}
void topu(){
	stack<int> s;
	for(int i = 1;i<=cnt;i++){
		if(!in[i]){
			s.push(i);
			f[i] = siz[i];
			ans[i] = 1;
		}
	}
	while(!s.empty()){
		int u = s.top();
		s.pop();
		for(int i = h2[u];~i;i=ne2[i]){
			int y = e2[i];
			if(f[u] + siz[y] > f[y]){
				f[y] = f[u] + siz[y];
				ans[y] = ans[u];
			}else if(f[u] + siz[y] == f[y]){
				ans[y] += ans[u];
				ans[y] %= p;
			}
			if(--in[y] == 0)s.push(y);
		}
	}
}
int main(){
	cin >> n >> m >> p;
	memset(h,-1,sizeof h);
	memset(h2,-1,sizeof h2);
	for(int i = 1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	for(int i = 1;i<=n;i++){
		if(!dfn[i])tarjan(i);
	}
	for(int i = 1;i<=n;i++){
		for(int j = h[i];~j;j=ne[j]){
			int y = e[j];
			if(scc[y] != scc[i] && !mp.count({scc[y],scc[i]})){
				in[scc[y]]++;
                mp[{scc[y],scc[i]}] = 1;
				add2(scc[i],scc[y]);
			}
		}
	}
	topu();
	long long mx=0,res=0;
	for(int i = 1;i<=cnt;i++){
		if(f[i] > mx){
			mx = f[i];
			res = ans[i];
		}else if(f[i] == mx){
			res += ans[i];
			res %= p;
		}
	}
	cout << mx << '\n' << res << endl;
	return 0;
}

嗅探器 (tips : 圆方树)

某军搞信息对抗实战演习,红军成功地侵入了蓝军的内部网络,蓝军共有两个信息中心,红军计划在某台中间服务器上安装一个嗅探器,从而能够侦听到两个信息中心互相交换的所有信息,但是蓝军的网络相当的庞大,数据包从一个信息中心传到另一个信息中心可以不止有一条通路。现在需要你尽快地解决这个问题,应该把嗅探器安装在哪个中间服务器上才能保证所有的数据包都能被捕获?

输入格式
输入文件的第一行一个整数 nn,表示蓝军网络中服务器的数目。

接下来若干行是对蓝军网络的拓扑结构描述,每行是两个整数 i,ji,j,表示编号为 ii 和编号为 jj 的两台服务器间存在连接(显然连接是双向的),服务器的编号从 11 开始,一行两个 00 表示网络的拓补结构描述结束,再接下来是两个整数 a,ba,b,分别表示两个中心服务器的编号。

输出格式
输出编号。如果有多个解输出编号最小的一个,如果找不到任何解,输出 No solution。

思路:

建立圆方树,从一个中心服务器dfs到另一个,记录上的割点。

代 码 代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
//526526
using namespace std;
const int N = 2e5+10;;
int h[N],e[N<<1],ne[N<<1],idx;
vector<int> bcc[N<<1];
int n;
int px,py;
int dfn[N],low[N],tim;
int stk[N],top;
int cnt;
void add(int a,int b){
	ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
void tarjan(int u){
	dfn[u] = low[u] = ++ tim;
	stk[++top] = u;
	for(int i=h[u];~i;i=ne[i]){
		int v = e[i];
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[v],low[u]);
			if(low[v] == dfn[u]){
				++ cnt;
				int y;
				do{
					y = stk[top--];
					bcc[cnt].push_back(y);
					bcc[y].push_back(cnt);
				}while(y != v);
				bcc[u].push_back(cnt);
				bcc[cnt].push_back(u);
			}
		}else low[u] = min(low[u],dfn[v]);
	}
}
void dfs(int s,int fa,int ans){
	if(s == py){
		if(ans == 0x3f3f3f3f){
			cout << "No solution\n";
		}else{
			cout << ans << endl;
		}
		exit(0);
	}
	//
	if(s != px && s <= n && bcc[s].size() > 1){
		ans = min(s,ans);
	} 
	for(int i = 0;i < bcc[s].size();i++){
		int y = bcc[s][i];
		if(y == fa)continue;
		dfs(y,s,ans);
	}
}
int main()
{
	memset(h,-1,sizeof h);
	cin >> n;
	while(1){
		int x,y;
		cin >> x >> y ;
		if(x == 0 && y == 0)break;
		add(x,y),add(y,x);
	}
	cin >> px >> py;
	cnt = n;
	tarjan(px);
	if(!dfn[py]){
		cout << "No solution\n";
		return 0;
	}
	dfs(px,0,0x3f3f3f3f);
	return 0;
}

Network of Schools (tips : 强联通分量)

A number of schools are connected to a computer network. Agreements have been developed among those schools: each school maintains a list of schools to which it distributes software (the “receiving schools”). Note that if B is in the distribution list of school A, then A does not necessarily appear in the list of school B
You are to write a program that computes the minimal number of schools that must receive a copy of the new software in order for the software to reach all schools in the network according to the agreement (Subtask A). As a further task, we want to ensure that by sending the copy of new software to an arbitrary school, this software will reach all schools in the network. To achieve this goal we may have to extend the lists of receivers by new members. Compute the minimal number of extensions that have to be made so that whatever school we send the new software to, it will reach all other schools (Subtask B). One extension means introducing one new member into the list of receivers of one school.

输入描述:
The first line contains an integer N: the number of schools in the network . The schools are identified by the first N positive integers. Each of the next N lines describes a list of receivers. The line i+1 contains the identifiers of the receivers of school i. Each list ends with a 0. An empty list contains a 0 alone in the line.

输出描述:
Your program should write two lines to the standard output. The first line should contain one positive integer: the solution of subtask A. The second line should contain the solution of subtask B.

思路:

s u b A subA subA: 几个入度为0的点。
s u b B subB subB : 至少添加几个边使每个点能互相到达( m a x ( 入 度 为 0 的 点 , 出 度 为 0 的 点 ) max(入度为0的点,出度为0的点) max(00)
特殊情况 :给的图是强联通的答案为 ( 1 , 0 ) (1,0) (1,0);

代 码 代码

#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e5+10;
int h[N],ne[N],e[N],idx;
int dfn[N],low[N],tim;
int cnt,scc[N];
int in[N],out[N];
int stk[N],top;
bool st[N];
int n;
int ans1;
void add(int a,int b){
    ne[idx] = h[a],e[idx] = b,h[a] = idx++; 
}
void tarjan(int u){
    dfn[u] = low[u] =++tim;
    stk[++top] = u;
    st[u] = true;
    for(int i = h[u];~i;i=ne[i]){
        int v = e[i];
        if(!dfn[v]){
            tarjan(v);
            low[u] = min(low[u],low[v]);
        }else if(st[v])low[u] = min(low[u],dfn[v]);
    }
    if(dfn[u] == low[u]){
        int y;
        ++cnt;
        do{
            y = stk[top--];
            st[y] = false;
            scc[y] = cnt;
        }while(u!=y);
    }
}

int main(){
    cin >> n;
    memset(h,-1,sizeof h);
    for(int i = 1;i<=n;i++){
        int x;
        while(1){
            cin >> x;
            if(x == 0)break;
            add(i,x);
        }
    }
    for(int i = 1;i<=n;i++){
        if(!dfn[i]){
            tarjan(i);
        }
    }
    for(int i = 1;i<=n;i++){
        for(int j = h[i];~j;j=ne[j]){
            int y = e[j];
            if(scc[y] != scc[i]){
                ++in[scc[y]];
                ++out[scc[i]];
            }
        }
    }
    if(cnt == 1){
        cout << "1\n0\n" ;
        return 0;
    }
    int tp1=0,tp2=0;
    for(int i = 1;i<=cnt;i++){
    	if(in[i] == 0)tp1++;
    	if(out[i] == 0)tp2++;
    }
    cout << tp1 << '\n' << max(tp1,tp2) ;
    return 0;
}

Cow Ski Area (tips : 强联通分量)

Farmer John’s cousin, Farmer Ron, who lives in the mountains of Colorado, has recently taught his cows to ski. Unfortunately, his cows are somewhat timid and are afraid to ski among crowds of people at the local resorts, so FR has decided to construct his own private ski area behind his farm.
FR’s ski area is a rectangle of width W and length L of ‘land squares’ (1 <= W <= 500; 1 <= L <= 500). Each land square is an integral height H above sea level (0 <= H <= 9,999). Cows can ski horizontally and vertically between any two adjacent land squares, but never diagonally. Cows can ski from a higher square to a lower square but not the other way and they can ski either direction between two adjacent squares of the same height.
FR wants to build his ski area so that his cows can travel between any two squares by a combination of skiing (as described above) and ski lifts. A ski lift can be built between any two squares of the ski area, regardless of height. Ski lifts are bidirectional. Ski lifts can cross over each other since they can be built at varying heights above the ground, and multiple ski lifts can begin or end at the same square. Since ski lifts are expensive to build, FR wants to minimize the number of ski lifts he has to build to allow his cows to travel between all squares of his ski area.
Find the minimum number of ski lifts required to ensure the cows can travel from any square to any other square via a combination of skiing and lifts.

输入描述:

  • Line 1: Two space-separated integers: W and L
  • Lines 2…L+1: L lines, each with W space-separated integers corresponding to the height of each square of land.

输出描述:

  • Line 1: A single integer equal to the minimal number of ski lifts FR needs to build to ensure that his cows can travel from any square to any other square via a combination of skiing and ski lifts

思路:
和上一题的 s u b B subB subB一样,注意特殊情况(一个强联通)。
代 码 代码


#include<iostream>
#include<cstring>
using namespace std;
const int N = 520*520 + 10;
int n,m;
int in[N],out[N];
int mp[510][510];
bool st[510][510];
int scc[N];
int cnt;
int dir[4][2] = {{1,0},{0,1},{-1,0},{0,-1}};
void dfs(int x,int y){
    st[x][y] = true;
    for(int i = 0;i<4;i++){
        int px = x + dir[i][0], py = y + dir[i][1];
        if(px >= 1 && px <= n && py >= 1 && py <= m && mp[x][y] == mp[px][py] && !st[px][py])scc[px*m+py] = cnt,dfs(px,py);
    }
}
int main(){
    cin >> m >> n;
    for(int i = 1;i<=n;i++)
    {
        for(int j = 1;j<=m;j++)
            cin >> mp[i][j];
    }
    for(int i = 1;i<=n;i++){
        for(int j = 1;j<=m;j++){
            if(st[i][j])continue;
            cnt ++;
            scc[i*m+j] = cnt;
            dfs(i,j);
        }
    }
    for(int i = 1;i<=n;i++){
        for(int j = 1;j<=m;j++){
            for(int k = 0;k<4;k++){
                int px = i + dir[k][0], py = j + dir[k][1];
                if(px >= 1 && px <= n && py >= 1 && py <= m){
                	if(mp[i][j] > mp[px][py])
                    	++in[scc[px*m+py]],out[scc[i*m+j]]++;
                }
            }
        }
    }
    if(cnt == 1){cout << 0 << endl;return 0;}
    int tp1=0,tp2=0;
    for(int i = 1;i<=cnt;i++)
		if(!in[i])tp1++;
		else if(!out[i])tp2++;
    cout << max(tp2,tp1) << endl;
    return 0;
}

P4782 【模板】2-SAT 问题 (tips : 2-sat)

P4782 【模板】2-SAT 问题

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
//526526
using namespace std;
const int N = 2e6+10;
int h[N],e[N],ne[N],idx;
void add(int a,int b){
	ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
int dfn[N],low[N],tim;
int stk[N],top;
int scc[N],cnt;
bool st[N];

void tarjan(int x){
	dfn[x] = low[x] = ++ tim;
	stk[++top] = x;
	st[x] = true;
	for(int i = h[x];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y);
			low[x] = min(low[y],low[x]);
		}else if(st[y]) low[x] = min(dfn[x],low[u]);
	}
	if(dfn[x] == low[x]){
		int y ;
		++ cnt;
		do{
			y = stk[top--];
			st[y] = false;
			scc[y] = cnt;
		}while(x != y);
	}
}

int main(){
	int n,m;
	cin >> n >> m;
	memset(h,-1,sizeof h);
	for(int i = 1;i<=m;i++){
		int a,b,c,d;
		scanf("%d%d%d%d",&a,&b,&c,&d);
		if(b && d){
			//a 或 c 为真 说明 非c->a , 非a->c; 
			add(c+n,a);
			add(a+n,c);
		}
		if(b && !d){
			//a 或 非c 为真 : 非a - > 非c ,c-> a;
			add(a+n,c+n);
			add(c,a); 
		}
		if(!b && d){
			// 非a 或 c为真 : a -> c , 非c -> 非a
			add(a,c);
			add(c+n,a+n);
		}
		if(!b && !d){
			// 非a或非c为真 : a- > 非c , c ->非a
			add(a,c+n);
			add(c,a+n); 
		}
	}
	for(int i = 1;i<=2*n;i++)
		if(!dfn[i])
			tarjan(i);
	for(int i = 1;i<=n;i++)
		if(scc[i] == scc[i+n]){
  			printf("IMPOSSIBLE\n");
            return 0;
		}
	
	printf("POSSIBLE\n");   //否则就是有解 
    for(int i=1;i<=n;i++)
    {
        if(scc[i] > scc[i+n]) printf("0 ");  //强联通分量编号越小 -> 拓扑序越大 -> 越优 
        else printf("1 ");
    }
	return 0;
} 


P5782 [POI2001] 和平委员会

题目链接

#include<iostream>
#include<cstring>
using namespace std;
const int N = 2e4+10,M = 2e5+10;
int h[N],e[M],ne[M],idx;
void add(int a,int b){
	ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
int n,m;
int dfn[N],low[N],tim;
int stk[N],top;
bool st[N];
int cnt;
int scc[N];
void tarjan(int u){
	dfn[u] = low[u] = ++tim;
	stk[++top] = u;
	st[u] = true;
	for(int i = h[u];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y);
			low[u] = min(low[u],low[y]);
		}
		else if(st[y])low[u] = min(low[u],dfn[y]);
	}
	if(dfn[u] == low[u]){
		int y;
		++cnt;
		do{
			y = stk[top--];
			st[y] = false;
			scc[y] = cnt;
		}while(y!=u);
	}
}
int main(){
	cin >> n >> m;
	memset(h,-1,sizeof h);
	for(int i = 1;i<=m;i++){
		int a,b;
		cin >> a >> b;
		if(a&1 && b&1){
			add(a,b+1);
			add(b,a+1);
		}
		if(!(a&1) && b&1){
			add(a,b+1);
			add(b,a-1);
		}
		if(a&1 && !(b&1)){
			add(a,b-1);
			add(b,a+1);
		}
		if(!(a&1) && !(b&1)){
			add(a,b-1);
			add(b,a-1);
		}
	}
	for(int i = 1;i<=n*2;i++){
		if(!dfn[i])tarjan(i);
	}
	for(int i = 1;i<=n*2;i+=2){
		if(scc[i] == scc[i+1]){
			cout << "NIE" << endl;
			return 0;
		}
	}
	for(int i = 1;i<=n*2;i+=2){
		if(scc[i] > scc[i+1])cout << i << endl;
		else cout << i + 1 << endl;
	}
	return 0;
}

ACM-ICPC Asia Seoul Regional K TV Show Game (tips : 2-sat)

题目链接

题面:有 K K K 盏灯,每盏灯是红色或者蓝色,但是初始的时候不知道灯的颜色。有 n n n 个人,每个人选择 3 盏灯并猜灯的颜色。一个人猜对两盏灯或以上的颜色就可以获得奖品。判断是否存在一个灯的着色方案使得每个人都能领奖,若有则输出一种灯的着色方案。

思路

对于每个人的三个选择,两两配对(a,b)表示有a必右b.就是各2-sat的问题了。

代 码 代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
//526526
using namespace std;
const int N = 1e5+10;
int h[N],ne[N],e[N],idx;
void add(int a,int b){
	ne[idx] = h[a],e[idx] = b,h[a] = idx++;
} 
int dfn[N],low[N],tim;
int scc[N],cnt;
int stk[N],top;
bool st[N];
void tarjan(int x){
	dfn[x] = low[x] = ++tim;
	stk[++top] = x;
	st[x] = true;
	for(int i = h[x];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y);
			low[x] = min(low[x],low[y]);
		}else if(st[y])low[x] = min(low[x],dfn[y]);
	}
	if(low[x] == dfn[x]){
		int y;
		++cnt;
		do{
			y = stk[top--];
			st[y] = false;
			scc[y] = cnt;
		}while(y!=x);
	}
}
int n,m;
int a[4];
char s[4][4];
int main(){
	cin >> n >> m;
	memset(h,-1,sizeof h);
	for(int i = 1;i<=m;i++){
		for(int j = 1;j<=3;j++)scanf("%d %s",&a[j],s[j]);
		
		//建立6条边
		for(int j = 1;j<=3;j++)
			for(int k = 1;k<=3;k++){
				if(k == j)continue;
				int u = a[j] + n * (s[j][0] == 'R');
				int v = a[k] + n * (s[k][0] == 'B');
				add(u,v);
			} 
	}
	for(int i = 1;i<=2*n;i++){
		if(!dfn[i])tarjan(i);
	}
	for(int i = 1;i<=n;i++){
		if(scc[i] == scc[i+n]){
			puts("-1");
			return 0;
		}
	}
	for(int i = 1;i<=n;i++){
		if(scc[i] > scc[i+n])printf("B");
		else printf("R");
	}
	return 0;
}
/*
7 5
3 R 5 R 6 B
1 B 2 B 3 R
4 R 5 B 6 B
5 R 6 B 7 B
1 R 2 R 4 R
*/

Redundant Paths (tips : 边双连通分量)

链接:https://ac.nowcoder.com/acm/problem/51307
来源:牛客网

题目描述
In order to get from one of the F (1 \leq F \leq 5,000)F(1≤F≤5,000) grazing fields (which are numbered 1…F) to another field, Bessie and the rest of the herd are forced to cross near the Tree of Rotten Apples. The cows are now tired of often being forced to take a particular path and want to build some new paths so that they will always have a choice of at least two separate routes between any pair of fields. They currently have at least one route between each pair of fields and want to have at least two. Of course, they can only travel on Official Paths when they move from one field to another.
Given a description of the current set of R (F-1 \leq R \leq 10,000)R(F−1≤R≤10,000) paths that each connect exactly two different fields, determine the minimum number of new paths (each of which connects exactly two fields) that must be built so that there are at least two separate routes between any pair of fields. Routes are considered separate if they use none of the same paths, even if they visit the same intermediate field along the way.
There might already be more than one paths between the same pair of fields, and you may also build a new path that connects the same fields as some other path.

输入描述:
Line 1: Two space-separated integers: F and R
Lines 2…R+1: Each line contains two space-separated integers which are the fields at the endpoints of some path.

输出描述:
Line 1: A single integer that is the number of new paths that must be built.

思路:使用边联通分量缩点,找到度数为1的点,最后结果为(res+1)/2;

代码

#include<iostream>
#include<cstring>
using namespace std;
const int N = 5e3+10,M = 2e4+10;
int h[N],e[M],ne[M],idx;
int dfn[N],low[N],tim;
int stk[N],top;
bool is_bridge[M];
int dcc[N],cnt;
int d[N];
void add(int a,int b){
    ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
//from:到u的这条边
void tarjan(int u,int from){    
    dfn[u] = low[u] = ++tim;
    stk[++top] = u;
    for(int i = h[u];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y,i);
            low[u] = min(low[u],low[y]);
            //桥
            if(dfn[u] < low[y])
               is_bridge[i] = is_bridge[i^1] = true;
        }
        //i不是返回u的那条反向边
        else if(i != (from ^ 1)) low[u] = min(dfn[u],low[y]);
    }
    //边联通分量 缩点
    if(low[u] == dfn[u]){
        int y;
        cnt++;
        do{
            y = stk[top--];
            dcc[y] = cnt;
        }while(y!=u); 
    }
}
int n,m;
int main(){
    cin >> n>>m;
    memset(h,-1,sizeof h);
    for(int i = 1;i<=m;i++){
        int a,b;
        cin >> a>>b;
        add(a,b);add(b,a);
    }
    tarjan(1,-1);
    
    //遍历每条边
    for(int i = 0;i<idx;i++){
        if(is_bridge[i])
           d[dcc[e[i]]] ++;
    }
    int ans = 0;
    for(int i = 1;i<=cnt;i++){
        if(d[i] == 1)ans ++;
    }
    cout << (ans+1)/2 << endl;
    return 0;
}


电力 (tips : 割点)

题目链接

题目描述
求一个图删除一个点之后,联通块最多有多少。

输入描述:
多组数据。第一行两个整数P,C表示点数和边数。
接下来C行每行两个整数p1,p2,表示p1与p2有边连接,保证无重边。读入以0 0结束。

输出描述:
输出若干行,表示每组数据的结果。

// 求割点 答案为 : 联通块数 + 割点连的边数 - 1
#include<iostream>
#include<cstring>
using namespace  std;
const int N = 1e4+10,M = 3e4+10;
int h[N],ne[M],e[M],idx;
int dfn[N],low[N],tim;
void add(int a,int b){
    ne[idx] = h[a] , e[idx] = b,h[a] = idx++;
}
int n,m;
int root,ans;
void tarjan(int u){
    dfn[u] = low[u] = ++ tim;
    int cnt = 0;
    for(int i = h[u];~i;i=ne[i]){
        int y = e[i];
            if(!dfn[y]){
                tarjan(y);
                low[u] = min(low[y],low[u]);
                if(low[y] >= dfn[u])cnt ++;//割点链接的联通快数
            }else low[u] = min(dfn[y],low[u]) ;
    }
    if(u != root && cnt)cnt ++; //不是根节点的上面还有一个连通块
    ans = max(ans,cnt);
}
int main(){
    while(cin >> n >> m){
        if(n==0)return 0;
        memset(dfn,0,sizeof dfn);
        memset(h,-1,sizeof h);
        idx = tim = 0;
        for(int i = 1;i<=m;i++){
            int a,b;
            cin >> a >>b;
            add(a,b),add(b,a);
        }
        int cnt1 = 0;
        ans = 0;
        for(root = 0;root<n;root++){
            if(!dfn[root]){
                cnt1++;
                tarjan(root);
            }
        }
        cout << cnt1 + ans - 1 << endl;
    }
    return 0;
}

[HNOI2012]矿场搭建 (tips : 点双联通分量)

题目链接

题目描述
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。

输入格式
输入文件有若干组数据,每组数据的第一行是一个正整数 N(N<=500),表示工地的隧道数,接下来的 N 行每行是用空格隔开的两个整数 S 和 T,表示挖 S 与挖煤点 T 由隧道直接连接。输入数据以 0 结尾。

输出格式
输入文件中有多少组数据,输出文件 output.txt 中就有多少行。每行对应一组输入数据的 结果。其中第 i 行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i 与:之间无空格,: 之后有空格),其后是用空格隔开的两个正整数,第一个正整数表示对于第 i 组输入数据至少需 要设置几个救援出口,第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总 数。输入数据保证答案小于 2^64。输出格式参照以下输入输出样例。

思路:
在这里插入图片描述

代 码 代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
//526526
using namespace std;
typedef unsigned long long ull;
const int N = 1010,M = 510;
int h[N],ne[M],e[M],idx;
int dfn[N],low[N],tim;
int stk[N],top;
vector<int > dcc[N];
int cut[N];
int cnt;
int root;
int n,m;
void add(int a,int b){
	ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
void tarjan(int u){
	dfn[u] = low[u] = ++tim;
	stk[++top] = u;
	int son=0;
		
	for(int i=h[u];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y);
			low[u] = min(low[y],low[u]);
			if(low[y] >= dfn[u]){
				son ++;
				if(u != root || son > 1)cut[u] = true;
				int t;
				++cnt;
				do{
					t = stk[top--];
					dcc[cnt].push_back(t);
				}while(y!=t);
				dcc[cnt].push_back(u);		
			}
		}else low[u] = min(low[u],dfn[y]);
	}
}
int main()
{
	int t = 0;
	while(cin >> m){
		if(m == 0)return 0;
		for(int i = 1;i<=n;i++)dcc[i].clear();
		n = tim = top = cnt = idx = 0;
		memset(dfn,0,sizeof dfn);
		memset(h,-1,sizeof h);
		memset(cut,0,sizeof cut);
		
		for(int i = 1;i<=m;i++){
			int a,b;
			cin >> a >> b;
			n = max(max(a,b),n);
			add(a,b),add(b,a);
		}
		ull num = 0 ,ans = 1;
		for(root = 1;root<=n;root++){
			if(!dfn[root]){
				tarjan(root);
			}
		}
		for(int i = 1;i<=cnt;i++){
			int tp = 0;
			//找几个割点 
			for(int j = 0;j<dcc[i].size();j++){
				if(cut[dcc[i][j]])tp++;
			}
			//0个割点 
			if(tp == 0)num += 2,ans *= (dcc[i].size()-1)*dcc[i].size()/2; 
			else if(tp == 1)num += 1,ans *= (dcc[i].size()-1);
		}
		printf("Case %d: %d %llu\n",++t,num,ans);
	}
	return 0;
}

[HAOI2010]软件安装 (tip : 强连通分量)

题目链接

思路:

先缩点,在树形背包。

树形背包模板:

void dfs(int u){
	for(int i = w2[u];i<=m;i++){
		f[u][i] = v2[u];
	}
	for(int i = h2[u];~i;i=ne2[i]){
		int y = e2[i];
		dfs(y);
		for(int j = m - w2[u];j >= 0;j--)
			for(int k = 0;k<=j;k++)
				f[u][j + w2[u]] = max(f[u][j + w2[u]],f[u][j + w2[u] - k] + f[y][k]); 
	}
}

代 码 代码

#include<iostream>
#include<cstring>
#include<map>
using namespace std;
const int N = 110,M = 510;
int h[N],ne[M],e[M],idx;
int h2[N],ne2[M],e2[M],idx2;
int dfn[N],low[N],tim;
int scc[N],cnt;
int n,m;
int stk[N],top;
bool st[N];
int w[N],v[N];
int w2[N],v2[N];
int f[N][M];
int d[N];
map<pair<int,int>,int> mp;
void add(int a,int b){ 
    ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
void add2(int a,int b){ 
    ne2[idx2] = h2[a],e2[idx2] = b,h2[a] = idx2++;
}
void tarjan(int x){
    dfn[x] = low[x] = ++ tim;
    stk[++top] = x;
    st[x] = true;
    for(int i = h[x];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y);
            low[x] = min(low[y],low[x]);
        }else if(st[y])low[x] = min(low[x],dfn[y]);
    }
    if(low[x] == dfn[x]){
       int y;
       cnt++;
        do{
           y = stk[top--];
           st[y] = false;
           scc[y] = cnt;
           w2[cnt] += w[y];
           v2[cnt] += v[y];
        }while(y!=x);
    }
}
void dfs(int u){
	for(int i = w2[u];i<=m;i++){
		f[u][i] = v2[u];
	}
	for(int i = h2[u];~i;i=ne2[i]){
		int y = e2[i];
		dfs(y);
		for(int j = m - w2[u];j >= 0;j--)
			for(int k = 0;k<=j;k++)
				f[u][j + w2[u]] = max(f[u][j + w2[u]],f[u][j + w2[u] - k] + f[y][k]); 
	}
}
int main(){
    cin >> n >> m;
    int tp;
    memset(h,-1,sizeof h);
    memset(h2,-1,sizeof h2);
    for(int i = 1;i<=n;i++)cin >> w[i];
    for(int i = 1;i<=n;i++)cin >> v[i];
    for(int i = 1;i<=n;i++){cin>> tp ;if(tp) add(tp,i);}
    for(int i = 1;i<=n;i++)if(!dfn[i])tarjan(i);
    
    for(int i = 1;i<=n;i++){
    	for(int j=h[i];~j;j=ne[j]){
    		int y = e[j];
    		//去重边
    		if(scc[y] != scc[i] && !mp.count({scc[y],scc[i]})){
    			add2(scc[i],scc[y]);
    			d[scc[y]] ++ ;
				mp[{scc[y],scc[i]}] = 1;
    		}
    	}
    }
    for(int i = 1;i<=cnt;i++)
    	if(!d[i])
			add2(0,i);
	dfs(0);
    cout << f[0][m] << endl;
    return 0;
}
/*
3 10
5 5 6
2 3 4
0 1 1
*/

Knights of the Round Table (tips:点联通分量 + 二分图)

题目链接

结论:

1.点联通分量内有奇环,所有点至少在一个简单奇环内。
2.二分图内无奇环。

思路

对无仇敌关系的骑士之间建边。跑点双连通分量,使用二分图判断每个点双是否有奇环,没有表明他们可以一起开会。
代 码 代码

#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N = 1e3+10,M = 2e6+10;
int h[N],ne[M],e[M],idx;
int n,m;
int dfn[N],low[N],tim;
int stk[N],top;
bool mp[N][N],st[N][N],vis[N];
vector<int> dcc[N];
int cnt;
int color[N];
void add(int a,int b){
    ne[idx] = h[a],e[idx] = b,h[a] = idx++;
} 
void tarjan(int x){
    dfn[x] = low[x] = ++tim;
    stk[++top] = x;
    for(int i = h[x];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y);
            low[x] = min(low[x],low[y]);
            if(low[y] >= dfn[x]){
                int t;
                ++cnt;
                do{
                    t = stk[top--];
                    dcc[cnt].push_back(t);
                    st[cnt][t] = true;
                }while(t!=y);
                dcc[cnt].push_back(x);
                st[cnt][x] = true;
            }
        }else low[x] = min(low[x],dfn[y]);
    }
}
bool dfs(int u,int col,int k){
    color[u] = col;
    for(int i = h[u];~i;i=ne[i]){
        int y = e[i];
        if(st[k][y]){
            if(!color[y]){
            	if(!dfs(y,3-col,k))return false;
        	}
			else if(color[y] == color[u])return false;
		}
    }
    return true;
}
int main(){
    while(cin >> n >> m){
        if(n == 0)return 0;
        memset(h,-1,sizeof h);
        memset(st,0,sizeof st);
        memset(mp,0,sizeof mp);
        memset(dfn,0,sizeof dfn);
        memset(vis,0,sizeof vis);
        memset(color,0,sizeof color);
        cnt = idx = tim = top = 0;
        for(int i = 1;i<=n;i++)dcc[i].clear();
        for(int i = 1;i<=m;i++){
            int a,b;
            scanf("%d%d",&a,&b);
            mp[a][b] = 1;
            mp[b][a] = 1;
        }
        for(int i = 1;i<=n;i++)
            for(int j = 1;j<=n;j++)
                if(!mp[i][j] && i!=j)add(i,j);
        for(int i = 1;i<=n;i++){
            if(!dfn[i])tarjan(i);
        }
        int ans = 0;
        for(int i = 1;i<=cnt;i++){
        	if(dcc[i].size() <= 1)continue;
        	for(int j = 0;j<dcc[i].size();j++)color[dcc[i][j]] = 0;
            if(!dfs(dcc[i][0],1,i)){
            	for(int j = 0;j<dcc[i].size();j++)vis[dcc[i][j]] = 1;
            }
        }
        for(int i = 1;i<=n;i++)if(!vis[i])ans ++;
        printf("%d\n",ans);
    }
    return 0;
}

Street Directions (tips : 桥)

uva 题目链接
题意

给定一个无向图(默认联通),请给尽可能多的无向边定向,使得定向后新图中所有点可以互相到达。并输出所有的边,已定向的输出i j的形式,不能定向的同时输出i j和j i (本题有SPJ) 另外提示一下,输出时在case编号后是有一个空行的。 洛谷Translated by @feng_chengjie~fixed by @fmj_123

思路:

桥必须双向,其他的边单向(注意方向)

代 码 代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
//526526
using namespace std;
const int N = 1e3+10,M = 2e6+10;
int h[N],e[M],ne[M],idx;
int st[M];
void add(int a,int b){
	ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
int dfn[N],low[N],tim;
bool is_bridge[M];
void tarjan(int u,int from){
	dfn[u] = low[u] = ++tim;
	for(int i = h[u];~i;i=ne[i]){
		int y = e[i];
		if(!dfn[y]){
			tarjan(y,i);
			low[u] = min(low[u],low[y]);
			if(low[y] > dfn[u])is_bridge[i] = is_bridge[i^1] = true;
		}else if(i != (from ^ 1))low[u] = min(low[u],dfn[y]);
	}
}
void dfs(int u){
	for(int i = h[u];~i;i=ne[i]){
		if(st[i])continue;
		int y = e[i];
		if(!is_bridge[i]){
			printf("%d %d\n",u,y);
		}else{	
			printf("%d %d\n",u,y);
			printf("%d %d\n",y,u);
		}
		st[i] = st[i^1] = true;
		dfs(y);
	}
}
int n,m;
int main(){
	int cs = 1;
	while(cin >> n >> m){
		if(n == 0)return 0;
		idx = tim = 0;
		memset(h,-1,sizeof h);
		memset(dfn,0,sizeof dfn);
		memset(is_bridge,0,sizeof is_bridge);
		memset(st,0,sizeof st);
		for(int i = 1;i<=m;i++){
			int a,b;
			scanf("%d%d",&a,&b);
			add(a,b),add(b,a);
		}		
		for(int i = 1;i<=n;i++)if(!dfn[i])tarjan(i,-1);
		printf("%d\n\n",cs++);
		for(int i = 1;i<=n;i++){
			dfs(i);
		}
		printf("#\n");
	}
	return 0;
}

King’s Quest (tip:强联通分量)

poj 题目链接

思路:

每个王子向喜欢的人连一边,对于爱人向该王子连一条边。
最后王子和在同一联通块内的公主都可以结婚。

代 码 代码

#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm> 
using namespace std;
const int N = 4e3+10,M = 3e5+10;
int h[N],e[M],ne[M],idx;
void add(int a,int b){
    ne[idx] = h[a],e[idx] = b,h[a] = idx++;
}
int n,delat = 2e3;
int dfn[N],low[N],tim;
int stk[N],top;
int scc[N],cnt;
bool st[N];
vector<int> ans[N];
void tarjan(int u){
    dfn[u] = low[u] = ++tim ;
    stk[++top] = u;
    st[u] = true;
    for(int i = h[u];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y);
            low[u] = min(low[u],low[y]);
        }else if(st[y])low[u] = min(low[u],dfn[y]);
    }
    if(low[u] == dfn[u]){
        int y;
        cnt ++;
        do{
            y = stk[top--];
            st[y] = false;
            scc[y] = cnt;
        }while(y!=u);
    }
}
int main(){
    cin >>  n;
    int k,tp;
    memset(h,-1,sizeof h);
    for(int i = 1;i<=n;i++){
        cin >> k;
        for(int j = 1;j<=k;j++){
        	scanf("%d",&tp);
            add(i,tp+delat);
        }
    }
    for(int i = 1;i<=n;i++){
        scanf("%d",&tp);
        add(tp+delat,i);
    }
    for(int i = 1;i<=n;i++){
        if(!dfn[i])
			tarjan(i);
    }
    for(int i = 1;i<=n;i++){
        for(int j = h[i];~j;j=ne[j]){
            int y = e[j];
            if(scc[y] == scc[i])ans[i].push_back(y);
        }
    }
    for(int i = 1;i<=n;i++){
        sort(ans[i].begin(),ans[i].end());
        printf("%d",ans[i].size());
        for(int j = 0;j<ans[i].size();j++)
            printf(" %d",ans[i][j] - delat);
        puts("");
    }
    return 0;
}

Line Graph Matching

icpc2021 沈阳
题目:
给你 n n n 个点, m m m 条边的带权无向连通图,将其转换为线型图,原图中的点变成边,边变成点,权值为原图两个边的权值和。求线图中的最大权点匹配(选取边,能有重复顶点,使得总权值最大)。

n ≤ 1 0 5 , m ≤ 2 ∗ 1 0 5 n≤10^5,m≤2∗10^5 n105,m2105

发现转化在原图中操作就是找两条有公共顶点的边。


  1. 分情况讨论
    [1]. 对于偶数条边,则权值和为最终结果。
    [2]. 奇数条边要选择删除几条边。

  1. 对与奇数边
    [1]. 如果删除的边不是割边,则任意删除一条(最小的)。
    [2]. 如果为割边,如果删除左右的边数都是偶数,则删除这一条就可以。
    [3]. 如果为有一边为奇数(不可能同时两边都为奇数 [奇数=奇数+偶数]),则继续删除奇数联通分量的边知道分成两个偶数,这样其实可以删除最后那条边就可以变成偶数,其实也是删一条(转变成[2])。

  1. 难点就变成找连通分量的边的个数
    可以在 t a r j a n tarjan tarjan时记录已经遍历的边,把孩子的边数加到父亲上,对于返祖边特变判定一下.
#include<iostream>
#include<cstring>
using namespace std;
const int N = 4e5+10;
int h[N],e[N],w[N],ne[N],idx;
void add(int a,int b,int c){
    ne[idx] = h[a],e[idx] = b,w[idx] = c,h[a] = idx++;
}
int n,m;
long long res;
int mn = 0x3f3f3f3f;
int dfn[N],low[N],tim;
int stk[N],top;
int dcc[N],cnt;
int siz[N];//记录以u所在的连通块的边得个数(相当于dfs更新图,碰到一个边添加到u)。
bool is_bridge[N];
void tarjan(int u,int from){
    dfn[u] = low[u] = ++tim;
    for(int i = h[u];~i;i=ne[i]){
        int y = e[i];
        if(!dfn[y]){
            tarjan(y,i);
            siz[u] += siz[y] + 1;
            low[u] = min(low[u],low[y]);
            if(dfn[u] < low[y]){
                if(siz[y] % 2 == 0)mn = min(mn,w[i]);
            }else mn = min(mn,w[i]);
        }
        else if(i != (from^1)){//返祖边
           if(dfn[u] > dfn[y])siz[u] ++;//这条边给后遍历的点。
           mn = min(mn,w[i]);
           low[u] = min(dfn[y],low[u]);
        }
    }
}

int main(){
    cin >> n >> m;
    memset(h,-1,sizeof h);
    for(int i = 1,a,b,c;i<=m;i++){
        cin >> a >> b >> c;
        add(a,b,c),add(b,a,c);
        res += c;
    }
    tarjan(1,-1);
    if(m&1)res -= mn;
    cout << res ;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值