1.二分图
2.网络流
2.最短路 双调旅dp
4.tarjan求强连通分量 割点 割边(桥) 点与边双连通分量
5.2-sat
6.最小树形图
7.(最小)生成树计数
8.带花树 一般图匹配
9.km算法
图论之二分图
(1) 二分图的最小路径覆盖
1.最小不相交路径覆盖 :Res == 节点数-最大匹配数
证明:一开始每个点都是独立的为一条路径,总共有n条不相交路径。我们每次在二分图里找一条匹配边就相当于把两条路径合成了一条路径,也就相当于路径数减少了1。所以找到了几条匹配边,路径数就减少了多少。所以有最小路径覆盖=原图的结点数-新图的最大匹配数
2.最小可相交路径覆盖:首先floyd算法跑出所有可以到达的点,之后Res==节点数-最大匹配数
算法:先用floyd求出原图的传递闭包,即如果a到b有路径,那么就加边a->b。然后就转化成了最小不相交路径覆盖问题。
证明:为了连通两个点,某条路径可能经过其它路径的中间点。比如1->3->4,2->4->5。但是如果两个点a和b是连通的,只不过中间需要经过其它的点,那么可以在这两个点之间加边,那么a就可以直达b,不必经过中点的,那么就转化成了最小不相交路径覆盖。
(2)二分图的最小顶点覆盖:
定义:若选择一个点说明选择与它相连的所有边,最小顶点覆盖就是选择最少的点来覆盖所有的边。
最小定点覆盖==二分图最大匹配
证明:König算法
(3)二分图最大独立集
定义:选出一些顶点使得这些顶点两两不相邻,则这些点构成的集合称为独立集。最大独立集为包含顶点数最多的独立集。
简单证明:
最大独立集合的定义 是 最大无关系点的集合。
而最小点覆盖的定义是,二分图中每个边至少一个端点在该点集中 的 最小点集。
如果去掉这些点, 相应的关系(边)也都没有了。剩下的点之间就相互没有关系,变成了独立集。
因为去掉的是 最少的点的集合, 所以剩下的就是最大独立集合。
最大独立集 就可以通过匈牙利算法来做
定理:最大独立集 = 所有顶点数 - 最小顶点覆盖 =最小路径覆盖
(4)二分图的最大团
定义: 团:选出一些点,使其两两之间都有边。 最大团:点数最大的团
定理:二分图的最大团 = 补图的最大独立集
感性理解:最大独立集为两两之间没有边,那么补图的最大独立集说明在原图中两两之间有边,那么就是原图的最大团
二分图的补图的两个 部分是两个最大团
(5)极大匹配(Maximal Matching):是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数。
(6)最大匹配(maximum matching):是所有极大匹配当中边数最大的一个匹配,设为M。选择这样的边数最大的子集称为图的最大匹配问题。
(7)完美匹配(完备匹配):一个图中所有的顶点都是匹配点的匹配,即2|M| = |V|。完美匹配一定是最大匹配,但并非每个图都存在完美匹配。
(8)最优匹配:最优匹配又称为带权最大匹配,是指在带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大。一般X和Y集合顶点个数相同,最优匹配也是一个完备匹配,即每个顶点都被匹配。如果个数不相等,可以通过补点加0边实现转化。一般使用KM算法解决该问题。(KM(Kuhn and Munkres)算法,是对匈牙利算法的一种贪心扩展。)
网络流
二分图可以使用hk算法降低M的边数来算
最大流主要是 找到 4个点 //多源最大流只需要 将多个源点链接新源点 多个汇点链接新汇点即可
1.源点 2.源点对于第一梯队的限制 3.第一梯队对第二梯队的影响 4.到汇点
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 1100,M = N * 5;
int n,m,S,T;
int head[M],to[M],last[M],w[M],cnt;
void add(int a,int b,int c){
to[++cnt] = b;
w[cnt] = c;
last[cnt] = head[a];
head[a] = cnt;
to[++cnt] = a;
w[cnt] = 0;
last[cnt] = head[b];
head[b] = cnt;
}
int d[N];int cur[N];
int bfs(){
memset(d,0,sizeof d);
queue<int>q;
q.push(S);
d[S] = 1;
cur[S] = head[S];
while(q.size()){
int p = q.front();
q.pop();
for(int i = head[p]; i != -1; i = last[i]){
int j = to[i];
if(!d[j] && w[i]){
d[j] = d[p] + 1;
cur[j] = head[j];
q.push(j);
if(j == T) return 1;
}
}
}
if(d[T]) return 1;
return 0;
}
int dfs(int x,int sum){
if(x == T) return sum;
int used = 0;
for(int i = cur[x]; i != -1; i = last[i]){
cur[x] = i;
int j = to[i];
if(d[j] == d[x] + 1 && w[i]){
int dd = dfs(j,min(sum - used,w[i]));
w[i] -= dd;
w[i ^ 1] += dd;
used += dd;
if(used == sum) break;
}
}
if(used == 0) d[x] = -1;
return used;
}
int dinic(){
int ans = 0;
while(bfs()){
ans += dfs(0,1);
}
return ans;
}
int main(){
scanf("%d%d",&n,&m);
memset(head,-1,sizeof head);
cnt = 1;
S = 0,T = m + 1;
int s = 0;
int x,y;
while(~scanf("%d%d",&x,&y)){
if(x == -1 && y == -1) break;
add(x,y,1);
++s;
}
for(int i = 1; i <= n; i++){
add(0,i,1);
}
for(int i = n + 1; i <= m; i++){
add(i,T,1);
}
cout << dinic() << endl;
for(int i = 2; i <= s * 2; i += 2){
if(!w[i]){
cout << to[i + 1] << " " << to[i] << endl;
}
}
return 0;
}
最大流之关键边(增加一个边的容量 若最大流增大 则为关键边)
首先若一个路S - T 经过 边 u - v 若 u - v不为满流 则 增加 u - v 是没有用的
若u - v是满流 但是 S - u 中有一条 边是满流的话 那么增加 u - v 也是没用的 若 v - T 有一条边是满流的话增加 u - v 也是没用的 所以只需要跑两个dfs 一个判断 S - u 是否满流 一个判断v - T是否满流
#include<iostream>
#include<cstring>
#include<queue>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 5e2 + 10,M = 4e5 + 10;
int head[N],to[M],last[M],w[M],cnt = 1;
int w2[M];
void add(int a,int b,int c){
to[++cnt] = b;
w[cnt] = c;
last[cnt] = head[a];
head[a] = cnt;
w2[cnt] = c;
to[++cnt] = a;
w[cnt] = 0;
last[cnt] = head[b];
head[b] = cnt;
w2[cnt] = 0;
}
int S,T;
int n,m,Sc,Tc;
int d[N],cur[N];
bool bfs(){
memset(d,0,sizeof d);
queue<int>q;
q.push(S);
d[S] = 1;
cur[S] = head[S];
while(q.size()){
int p = q.front();
q.pop();
for(int i = head[p]; i != -1; i = last[i]){
int j = to[i];
if(!d[j] && w[i]){
d[j] = d[p] + 1;
cur[j] = head[j];
q.push(j);
if(j == T) return true;
}
}
}
return false;
}
int dfs(int x,int sum){
if(x == T) return sum;
int used = 0;
for(int i = cur[x]; i != -1; i = last[i]){
cur[x] = i;
int j = to[i];
if(d[j] == d[x] + 1 && w[i]){
int dd = dfs(j,min(sum - used,w[i]));
w[i] -= dd;
w[i ^ 1] += dd;
used += dd;
if(used == sum) break;
}
}
if(used == 0) d[x] = -1;
return used;
}
int dinic(){
int sum = 0;
while(bfs()){
sum += dfs(S,INF);
}
return sum;
}
int vis_s[N],vis_t[N];
void dfss(int x,int st[],int f){
st[x] = 1;
for(int i = head[x]; i != -1; i = last[i]){
int b = i ^ f;
int j = to[i];
if(!st[j] && w[b]){
dfss(j,st,f);
}
}
}
int main(){
memset(head,-1,sizeof head);
int n,m;
cin >> n >> m;
S = 0,T = n - 1;
for(int i = 1; i <= m; i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
int k = dinic();
dfss(S,vis_s,0);
dfss(T,vis_t,1);
int sum = 0;
for(int i = 2; i < 2 + m * 2; i += 2){
if(!w[i] && vis_s[to[i ^ 1]] && vis_t[to[i]]) sum++;
}
cout << sum << endl;
return 0;
}
无源汇上下界可行流
是将 最小流量转换成 0 比如 a -> b 最小为1 最大为 3 那么进入 b可以改成 最小为 0 最大为2
b -> c 最小为2 最大为 4 那么b出最小为 0 最大为 2 此时可以看见 b这个点的流量是不守恒得 因为
进到b得少了 1 但是b出去的少了2 所以总体是 b 出去的少了1 所以我们需要建立 虚点 T让b这个点出去 若 b进去的多了 我们就要建立 S 让 S 进到 b里面 去 所以只需要判断是否为满流就行
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 210,M = 102000;
int ru[N],chu[N],S,T;
int head[N],to[M],last[M],l[M],w[M],cnt;
void add(int a,int b,int c,int d){
to[++cnt] = b;
w[cnt] = c - d;
l[cnt] = d;
last[cnt] = head[a];
head[a] = cnt;
to[++cnt] = a;
w[cnt] = 0;
last[cnt] = head[b];
head[b] = cnt;
}
int d[N],cur[N];
int bfs(){
memset(d,0,sizeof d);
queue<int>q;
d[0] = 1;
q.push(0);
cur[S] = head[S];
while(q.size()){
int p = q.front();
q.pop();
for(int i = head[p]; i != -1; i = last[i]){
int j = to[i];
if(!d[j] && w[i]){
d[j] = d[p] + 1;
cur[j] = head[j];
if(j == T) return 1;
q.push(j);
}
}
}
if(d[T]) return 1;
return 0;
}
int dfs(int x,int sum){
if(x == T) return sum;
int used = 0;
for(int i = cur[x]; i != -1; i = last[i]){
cur[x] = i;
int j = to[i];
if(d[j] == d[x] + 1 && w[i]){
int dd = dfs(j,min(sum - used,w[i]));
w[i] -= dd;
w[i ^ 1] += dd;
used += dd;
if(used == sum) break;
}
}
if(used == 0) d[x] = -1;
return used;
}
int dinic(){
int sum = 0;
while(bfs()){
sum += dfs(0,1e9);
}
return sum;
}
int main(){
int n,m;
memset(head,-1,sizeof head);
cnt = 1;
scanf("%d%d",&n,&m);
int sum = 0;
for(int i = 1; i <= m; i++){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
add(a,b,d,c);
ru[a] -= c;
ru[b] += c;
}
S = 0,T = n + 1;
for(int i = 1; i <= n; i++){
if(ru[i] > 0){
add(S,i,ru[i],0),sum += ru[i];
}else{
add(i,T,-ru[i],0);
}
}
if(dinic() == sum){
cout << "YES" << endl;
for(int i = 2; i <= m * 2; i += 2){
printf("%d\n",w[i ^ 1] + l[i]);
}
}else cout << "NO" << endl;
return 0;
}
有源汇上下界最大流最小流
首先他给出了汇点 和 源点 我们从汇点向源点 连一条无限大的边 那么这个新图就变成了无源汇上下界可行流 我们判断可行流后 去掉无限大的那条边 因为此时他已经是满流了 若我们继续用这个图去跑残留网络的时候 可能会回流 那时候就不满足满流了 此时再跑一遍 给出的s,t的最大流即可
证明 :新图f(g,v0,v1,v2; g是新图 此时 v0 v1 v2都可以是新图的满流 因为 流量守恒 所以1从
每个满流 v0 v1 从S流出的流量一定是等于 T流出的流量 并且 流量都是相同的 所以 v0 - v1的时候
从S流出的流量就变为了 0 ,从T流入的流量也变为了0 所以此时就变成了 给出的s,t的可行流
所以我们只需要找出 s,t 当前可行流的最大流 就可以找到 此题的最大流 若求最小流 只需要找t,s
的最大流 在满足满流的情况下尽可能的回流!
//有源汇上下界最大流的模板
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 210,M = 1e5 + 10;
int n,m,s,t,S,T;
int du[N];
int head[N],to[M],last[M],l[M],w[M],cnt;
void add(int a,int b,int c,int d){
to[++cnt] = b;
w[cnt] = d - c;
l[cnt] = c;
last[cnt] = head[a];
head[a] = cnt;
to[++cnt] = a;
w[cnt] = 0;
last[cnt] = head[b];
head[b] = cnt;
}
int d[N],cur[N];
int bfs(){
queue<int>q;
q.push(S);
memset(d,0,sizeof d);
d[S] = 1;
cur[S] = head[S];
while(q.size()){
int p = q.front();
q.pop();
for(int i = head[p]; i != -1; i = last[i]){
int j = to[i];
if(!d[j] && w[i]){
d[j] = d[p] + 1;
cur[j] = head[j];
q.push(j);
if(j == T) return 1;
}
}
}
if(d[T]) return 1;
return 0;
}
int dfs(int x,int sum){
int used = 0;
if(x == T) return sum;
for(int i = cur[x]; i != -1; i = last[i]){
int j = to[i];
cur[x] = i;
if(d[j] == d[x] + 1 && w[i]){
int dd = dfs(j,min(sum - used,w[i]));
w[i] -= dd;
w[i ^ 1] += dd;
used += dd;
if(used == sum) break;
}
}
if(!used) d[x] = -1;
return used;
}
int dinic(){
int sum = 0;
while(bfs()){
sum += dfs(S,1e9);
}
return sum;
}
int main(){
memset(head,-1,sizeof head);
cnt = 1;
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i = 1; i <= m; i++){
int a,b,c,d;
scanf("%d%d%d%d",&a,&b,&c,&d);
add(a,b,c,d);
du[b] += c;
du[a] -= c;
}
S = 0,T = n + 1;int sum = 0;
for(int i = 1; i <= n; i++){
if(du[i] > 0){
sum += du[i];
add(S,i,0,du[i]);
}
if(du[i] < 0){
add(i,T,0,-du[i]);
}
}
add(t,s,0,1e9);
if(dinic() >= sum){
sum = w[cnt];
w[cnt] = 0;
w[cnt - 1] = 0;
S = s,T = t;
sum += dinic();
printf("%d\n",sum);
}else{
cout << "No Solution" << endl;
}
return 0;
}
关于最短路的一些题目和方法
1.双调欧几里得旅行商问题
例如hdu2224 题目大意就是为 从1开始 走过 1 - n所有的点 而且 1 - n的时候必须 是 x逐渐增大 回来的时候 是 x逐渐减小 每个点只能走一次
求解过程
设dp(i,j) 为 i到1然后1到 j 的路 的最小值
dist[i][j] 为 i点 到 j 点的距离
1.当 j < i - 1的时候
dp[i][j] = dp[i - 1][j] + dist[i - 1][i];
因为 当 j < i - 1的时候 i的左邻点 一定为 i - 1
2.当 j = i - 1的时候
dp[i][j] = dp[i - 1][k] + dist[k][j];
这是为什么呢
因为 dp[i - 1][k]就是 i - 1到 1 再到 k
这也可以想成是 k 到 1 然后 1 到 i - 1 然后连上 k i
3 当 j == i == n的时候 其实就是最后一步
dp[n][n] = dp[n][j] + dist[j][1];
#include<iostream>
#include<cmath>
using namespace std;
const int N = 210;
double dist[N][N];
double x[N],y[N];
double dp[N][N];
int main(){
int n;
while(cin >> n){
for(int i = 1; i <= n; i++){
scanf("%lf%lf",&x[i],&y[i]);
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
dist[i][j] = sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]));
}
}
dp[2][1] = dist[1][2];
for(int i = 3; i <= n; i++){
for(int j = 1; j <= i - 2; j++){
//与i相连的左点 为 i - 1
dp[i][j] = dp[i - 1][j] + dist[i][i - 1];
}
//回路最右端 为 i - 1
dp[i][i - 1] = 1000000000.0;
for(int j = 1; j <= i - 2; j++){
dp[i][i - 1] = min(dp[i][i - 1],dp[i - 1][j] + dist[j][i]);
}
}
double ans = 1000000000.0;
for(int i = 1; i <= n - 1; i++){
ans = min(ans,dp[n][i] + dist[i][n]);
}
printf("%.2lf\n",ans);
}
return 0;
})
有向图得强连通分量,无向图得双连通分量
有向图的强连通分量
在有向图G中,如果两个顶点u,v间有一条从u到v的有向路径,同时还有一条从v到u的有向路径,则称两个顶点强连通。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向非强连通图的极大强连通子图,称为强连通分量
//求还需要加上几条边可以变成强连通图
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
const int N = 2e4 + 10,M = 5e4 + 10;
int max(int x,int y){
if(x > y) return x;
return y;
}
int min(int x,int y){
if(x > y) return y;
return x;
}
int head[N],to[M],last[M],cnt;
void add(int a,int b){
to[++cnt] = b;
last[cnt] = head[a];
head[a] = cnt;
}
int toopc[N],toopr[N];
int low[N],dfn[N],sta[N],times,flag[N],top,id[N],scc_cnt;
void tarjan(int x){
low[x] = dfn[x] = ++times;
sta[++top] = x,flag[x] = 1;
for(int i = head[x]; i != -1; i = last[i]){
int j = to[i];
if(!dfn[j]){
tarjan(j);
low[x] = min(low[x],low[j]);
}else if(dfn[j] && flag[j]){
low[x] = min(low[x],dfn[j]);
}
}
if(dfn[x] == low[x]){
++scc_cnt;
int y;
do{
y = sta[top--];
id[y] = scc_cnt;
flag[y] = 0;
}while(y != x);
}
}
int main(){
int t;
cin >> t;
int n,m;
while(t--){
memset(low,0,sizeof low);
memset(dfn,0,sizeof dfn);
memset(id,0,sizeof id);
times = 0;
cnt = 0,scc_cnt = 0;
memset(head,-1,sizeof head);
memset(toopr,0,sizeof toopr);
memset(toopc,0,sizeof toopc);
cin >> n >> m;
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 = head[i]; j != -1; j = last[j]){
int k = to[j];
int s1 = id[k],s2 = id[i];
if(s2 != s1) toopc[s2]++,toopr[s1]++;
}
}
int sum1 = 0,sum2 = 0,sum = 0;
for(int i = 1; i <= scc_cnt; i++){
if(!toopc[i]) sum1++;
if(!toopr[i]) sum2++;
}
if(scc_cnt == 1) cout << 0 << endl;
else{
cout << max(sum1,sum2) << endl;
}
}
return 0;
}
无向图求桥
#include<iostream>
#include<cstring>
#include<map>
using namespace std;
const int N = 1e4 + 10,M = 2e5 + 10,mod = 998244353;
typedef long long ll;
map<string,int>mp;
map<int,string>mp2;
int head[N],to[M],is[M],last[M],cnt;
void add(int a,int b){
to[++cnt] = b;
is[cnt] = a;
last[cnt] = head[a];
head[a] = cnt;
}
int sta[N],is_bridge[M],dfn[N],low[N],times;
void tarjan(int x,int ne){
dfn[x] = low[x] = ++times;
for(int i = head[x]; i != -1; i = last[i]){
int j = to[i];
if(!dfn[j]){
tarjan(j,i);
low[x] = min(low[x],low[j]);
if(dfn[x] < low[j]){
is_bridge[i] = is_bridge[i ^ 1] = 1;
}
}else if(i != (ne ^ 1)){
low[x] = min(low[x],dfn[j]);
}
}
}
int main(){
int t;
cin >> t;
while(t--){
mp.clear();
mp2.clear();
memset(is_bridge,0,sizeof is_bridge);
memset(head,-1,sizeof head);
cnt = 1;
int ans = 0;
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1; i <= m; i++){
string a,b;
cin >> a >> b;
if(!mp[a]){
mp[a] = ++ans;
mp2[ans] = a;
}
if(!mp[b]){
mp[b] = ++ans;
mp2[ans] = b;
}
add(mp[a],mp[b]);
add(mp[b],mp[a]);
}
tarjan(1,-1);
int s = 0;
for(int i = 2; i <= cnt; i += 2){
if(is_bridge[i]) ++s;
}
bool flag = true;
for(int i = 1; i <= n; i++){
if(!dfn[i]){
flag = false;
break;
}
}
if(!flag){
cout << 0 << endl;
continue;
}
cout << s << endl;
for(int i = 2; i <= cnt; i += 2){
if(is_bridge[i]) cout << mp2[is[i]] << " " << mp2[to[i]] << endl;
}
}
return 0;
}
无向图求割点
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N = 1010;
int n,maxn;
int head[N],to[N * 2],last[N * 2],cnt;
void add(int a,int b){
to[++cnt] = b;
last[cnt] = head[a];
head[a] = cnt;
}
vector<int>dcc[N];
int dfn[N],low[N],sta[N],cut[N];
int times,top,root,dcc_cnt;
void tarjan(int x){
dfn[x] = low[x] = ++times;
sta[++top] = x;
int ans = 0;
if(x == root && head[x] == -1){
++dcc_cnt;
dcc[dcc_cnt].push_back(x);
return;
}
for(int i = head[x]; i != -1; i = last[i]){
int j = to[i];
if(!dfn[j]){
tarjan(j);
low[x] = min(low[x],low[j]);
if(dfn[x] <= low[j]){
ans++;
if(x != root || ans > 1) cut[x] = 1;
++dcc_cnt;
int y;
do{
y = sta[top--];
dcc[dcc_cnt].push_back(y);
}while(y != j);
dcc[dcc_cnt].push_back(x);
}
}else low[x] = min(low[x],dfn[j]);
}
}
int main(){
int casse = 0;
while(cin >> n && n){
memset(head,-1,sizeof head);
memset(dfn,0,sizeof dfn);
memset(cut,0,sizeof cut);
maxn = 0,cnt = 0;
dcc_cnt = 0,times = 0,top = 0;
for(int i = 1; i <= n; i++){
int x,y;
cin >> x >> y;
maxn = max(maxn,x);
maxn = max(maxn,y);
add(x,y);
add(y,x);
}
for(root = 1; root <= maxn; root++){
if(!dfn[root]){
tarjan(root);
}
}
long long sum1 = 0,sum2 = 1;
for(int i = 1; i <= dcc_cnt; i++){
int h = 0;
for(int j = 0; j < dcc[i].size(); j++){
if(cut[dcc[i][j]] == 1) h++;
}
if(h == 0){
//无割点
if(dcc[i].size() > 1){
//为点双连通图 所以要取 2个 取2个防止坍塌的就是出口
sum1 += 2;
sum2 *= (dcc[i].size() * (dcc[i].size() - 1) / 2);
}else sum1++;
}else if(h == 1){
sum1++;
sum2 *= (dcc[i].size() - 1);
}
dcc[i].clear();
}
cout << "Case " << ++casse << ":" << " " << sum1 << " " << sum2 << endl;
}
return 0;
}
2-sat
- x1或x2 这种直接条件就直接转换为 不选x1必须选x2 和 不选x2 必须选 x1
- x1 -> x2这种类型 x1->x2 = 非x1|x2 = x2 | 非x1 则转换为 选了x1就要选x2 选了x2 就要选非x1即可
- x1 = 1 这种类型 就是 x1或x1 转换成 选了x1就要选x1即可 x1 | x1
- 然后求tarjan 即可`
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
const int N = 2010,M = 2010;
typedef long long ll;
typedef pair<int,int> PII;
typedef unsigned long long ull;
char a,b;
int head[N],to[M],last[M],cnt = 1;
void add(int a,int b){
to[++cnt] = b;
last[cnt] = head[a];
head[a] = cnt;
}
int n,m;
int dfn[N],low[N],sta[N],top,scc_cnt,times;
int flag[N],id[N];
void tarjan(int x){
dfn[x] = low[x] = ++times;
sta[++top] = x,flag[x] = 1;
for(int i = head[x]; i != -1; i = last[i]){
int j = to[i];
if(!dfn[j]){
tarjan(j);
low[x] = min(low[x],low[j]);
}else if(flag[j]){
low[x] = min(low[x],dfn[j]);
}
}
if(dfn[x] == low[x]){
++scc_cnt;
int y;
do{
y = sta[top--];
id[y] = scc_cnt;
flag[y] = 0;
}while(y != x);
}
}
int main(){
int t;
cin >>t;
while(t--){
scanf("%d%d",&n,&m);
memset(head,-1,sizeof head);
for(int i = 2; i <= n * 2 + 2; i++){
id[i] = 0;
dfn[i] = 0,low[i] = 0;
}
times = 0;
scc_cnt = 0;
cnt = 1;
for(int i = 1; i <= m; i++){
int c,d;
getchar();
scanf("%c%d %c%d",&a,&c,&b,&d);
if(a == 'm' && b == 'm'){
add(c * 2,d * 2 + 1);
add(d * 2,c * 2 + 1);
}else if(a == 'h' && b== 'm'){
add(c * 2 + 1,d * 2 + 1);
add(d * 2,c * 2);
}else if(a == 'm' && b == 'h'){
add(c * 2,d * 2);
add(d * 2 + 1,c * 2 + 1);
}else{
add(c * 2 + 1,d * 2);
add(d * 2 + 1,c * 2);
}
}
for(int i = 2; i <= n * 2 + 1; i++){
if(!dfn[i]) tarjan(i);
}
bool flag = true;
for(int i = 1; i <= n; i++){
if(id[i * 2] == id[i * 2 + 1]){
flag = false;
puts("BAD");
break;
}
}
if(flag) puts("GOOD");
}
return 0;
}
****
6.最小树形图模板
每次都找自己入边最小的边 若有环 则缩点 缩点后 若不是环上的边 则 减去上次 用过的最小的边
直到无环
#include<iostream>
#include<cstring>
#define INF 0x3f3f3f3f
using namespace std;
const int N = 1100;
int n,m;
struct node{
int x,y,v;
}a[N * N];
int L;
int w[N],pre[N],vis[N],id[N];
int zhuliu(int root){
int ans = 0;
while(1){
for(int i = 0; i < n; i++) w[i] = INF;
for(int i = 1; i <= L; i++){
if(a[i].x != a[i].y && w[a[i].y] > a[i].v){
w[a[i].y] = a[i].v;
pre[a[i].y] = a[i].x;
}
}
w[root] = 0;
int cnt = 0;
for(int i = 0; i < n; i++) if(i != root && w[i] == INF) return -1;
memset(id,-1,sizeof id),memset(vis,-1,sizeof vis);
for(int i = 0; i < n; i++){
ans += w[i];
int v = i;
while(vis[v] != i && id[v] == -1 && v != root){
//遍历一遍环 若是环的话 结尾不会到 root 若不是则到 root
vis[v] = i;
v = pre[v];
}
if(v != root && id[v] == -1){
//
for(int j = pre[v]; j != v; j = pre[j]){
id[j] = cnt;
}
id[v] = cnt++;
}
}
if(cnt == 0) break;
for(int i = 0; i < n; i++){
if(id[i] == -1) id[i] = cnt++;
}
for(int i = 1; i <= L;){
int x = a[i].x,y = a[i].y;
a[i].x = id[x],a[i].y = id[y];
if(id[x] != id[y]){
a[i++].v -= w[y];
}else swap(a[i],a[L--]); //因为是同一个环 所以点用不上了
}
n = cnt;
root = id[root];
}
return ans;
}
int dis[N][N];
int main(){
int t;
cin >>t;
for(int _ = 1; _ <= t; _++){
cin >> n >> m;
memset(dis,0x3f,sizeof dis);
for(int i = 1; i <= m; i++){
int a,b,v;
scanf("%d%d%d",&a,&b,&v);
if(a == b) continue;
dis[a][b] = min(dis[a][b],v);
}
L = 0;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
if(dis[i][j] < INF){
a[++L] = {
i,j,dis[i][j]};
}
}
}
int s = zhuliu(0);
if(s == -1)
cout << "Case #" << _ << ": " << "Possums!" << endl;
else cout << "Case #" << _ << ": " << s << endl;
}
return 0;
}
生成树计数
对于生成树的计数,一般采用矩阵树定理(Matrix-Tree 定理)来解决。
Matrix-Tree 定理的内容为:对于已经得出的基尔霍夫矩阵,去掉其随意一行一列得出的矩阵的行列式,其绝对值为生成树的个数
基尔霍夫矩阵 是由度矩阵 - 联通矩阵(邻接表)
度矩阵是对角线 对于 其他点有几条联通的边
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
typedef long long ll;
const int N = 400;
const double eps = 1e-6;
ll a[N][N];
int n,m;ll p;
void kill(ll a,ll b,ll &aii,ll & aij,ll &aji,ll &ajj,ll &pa){
pa = 1;
aii = ajj = 1;
aji = aij = 0;
while(b){
aii -= a / b * aji;
aij -= a / b * ajj;
a %= b;
swap(a,b);
swap(aii,aji);
swap(aij,ajj);
pa = -pa;
}
return;
}
ll guess(){
ll ret = 1,_a,b,c,d,s1,s2,pa;
for(int i = 1; i <= n; i++){
//当前列
for(int j = i + 1; j <= n; j++){
kill(a[i][i],a[j][i],_a,b,c,d,pa);
ret *= pa;
for(int k = 1; k <= n; k++){
s1 = a[i][k] * _a + a[j][k] * b;
s2 = a[i][k] * c + a[j][k] * d;
a[i][k] = s1,a[j][k] = s2;
}
}
if(a[i][i] == 0) return 0;
ret = ret * a[i][i];
}
return ret;
}
int main(){
int t;
cin >>t;
while(t--){
cin >>n >> m;
memset(a,0,sizeof a);
for(int i = 1; i <= m; i++){
int x,y;
scanf("%d%d",&x,&y);
a[x][y]--;
a[y][x]--;
a[x][x]++;
a[y][y]++;
}
--n;
cout << guess() <<endl;
}
return 0;
}
最小生成树计数
(1)不同的最小生成树中,每种权值的边出现的个数是确定的
(2)不同的生成树中,某一种权值的边连接完成后,形成的联通块状态是一样的
每次相同的权值的边 联通不同的连通块 那么就成为了生成树计数 用乘法原理相乘即可
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 400;
const double eps = 1e-6;
struct node{
int x,y,w,used;
bool operator < (const node &p)const{
return w < p.w;
}
}b[N * 10];
ll a[N][N];
int fa[N],ka[N],vis[N];
int get(int x,int fas[]){
if(fas[x] == x) return x;
return fas[x] = get(fas[x],fas);
}
int p;
void kill(ll a,ll b,ll &aii,ll & aij,ll &aji,ll &ajj,ll &pa){
pa = 1;
aii = ajj = 1;
aji = aij = 0;
while(b){
aii -= a / b * aji;
aij -= a / b * ajj;
aii = (aii % p + p) % p;
aij = (aij % p + p) % p;
a %= b;
swap(a,b);
swap(aii,aji);
swap(aij,ajj);
pa = -pa;
}
return;
}
ll guess(ll n){
ll ret = 1,_a,b,c,d,s1,s2,pa;
for(ll i=1;i<=n;++i)for(ll j=1;j<=n;++j)if(a[i][j]<0)a[i][j]+=p;
for(int i = 1; i <= n; i++){
//当前列
for(int j = i + 1; j <= n; j++){
kill(a[i][i],a[j][i],_a,b,c,d,pa);
ret *= pa;
for(int k = 1; k <= n; k++){
s1 = (a[i][k] * _a + a[j][k] * b) % p;
s2 = (a[i][k] * c + a[j][k] * d) % p;
a[i][k] = s1,a[j][k] = s2;
}
}
if(a[i][i] == 0) return 0;
ret = ret * a[i][i] % p;
}
return (ret + p) % p;
}
int n,m;
int G[N][N];
ll ans = 1;
vector<int>v[N];
void Matrix_Tree(){
for(int i = 1; i <= n; i++) v[i].clear();
for(int i = 1; i <= n; i++){
if(vis[i]){
v[get(i,ka)].push_back(i); //将多个联通块变成一个联通块连接在一起的点
vis[i] = 0;
}
}
for(int i = 1; i <= n; i++){
if(v[i].size() > 1){
int len = v[i].size();
for(int j = 1; j <= n; j++){
for(int k = 1; k <= n; k++) a[j][k] = 0;
}
for(int j = 0; j < len; j++){
for(int k = j + 1; k < len; k++){
生成树计数
int x = v[i][j],y = v[i][k];
a[j + 1][j + 1] += G[x][y];
a[k + 1][k + 1] += G[x][y];
a[k + 1][j + 1] -= G[x][y];
a[j + 1][k + 1] -= G[x][y];
}
}
ans = ans * guess(len - 1) % p;
}
}
for(int i = 1; i <= n; i++){
fa[i] = get(i,ka); //缩点将已经使用过的点缩成一点
}
}
int is[N];
int main(){
while(cin >> n >> m >> p){
if(n == 0 && m == 0 && p == 0) break;
for(int i = 1; i <= m; i++){
scanf("%d%d%d",&b[i].x,&b[i].y,&b[i].w);
b[i].used = 0;
}
ans = 1;
sort(b + 1,b + m + 1);
for(int i = 1; i <= n; i++) fa[i] = i,ka[i] = i;
int pre = b[1].w;
memset(G,0,sizeof G);
memset(vis,0,sizeof vis);
for(int i = 1; i <= m; i++){
int x = get(b[i].x,fa),y = get(b[i].y,fa);
if(x != y){
vis[x] = 1,vis[y] = 1;
ka[get(x,ka)] = get(y,ka);
G[x][y]++,G[y][x]++;
}
if(i == m || pre != b[i + 1].w){
//若权值不同则 进行一次生成树计数
Matrix_Tree();
pre = b[i + 1].w;
}
}
bool flag = true;
for(int i = 1; i < n; i++){
if(fa[i] != fa[i + 1]){
flag = false;
break;
}
}
if(!flag) cout << 0 <<endl;
else cout << (ans + p) % p << endl;
}
return 0;
}
带花树 一般图匹配
#include <iostream>
#include <cstring>
#include <deque>
#include <cmath>
#include <queue>
#include <stack>
#include <list>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <cstdio>
#include <bitset>
#include <algorithm>
using namespace std;
#define Debug(x) cout << #x << " " << x <<endl
#define Memset(x, a) memset(x, a, sizeof(x))
const int INF = 0x3f3f3f3f;
typedef long long LL;
typedef pair<int, int> P;
#define FOR(i, a, b) for(int i = a;i < b; i++)
#define lson l, m, k<<1
#define rson m+1, r, k<<1|1
#define MAX_N 200
const int MAXN = 450;
int N, M; //点的个数,点的编号从1到N
bool Graph[MAXN][MAXN];
int Match[MAXN];
bool InQueue[MAXN],InPath[MAXN],InBlossom[MAXN];
int Head,Tail;
int Queue[MAXN];
int Start,Finish;
int NewBase;
int Father[MAXN],Base[MAXN];
int Count;//匹配数,匹配对数是Count/2
int p_tot, q_tot;
struct Point{
int x, y;
}p[MAX_N], q[MAX_N];
void Push(int u)
{
Queue[Tail] = u;
Tail++;
InQueue[u] = true;
}
int Pop()
{
int res = Queue[Head];
Head++;
return res;
}
int FindCommonAncestor(int u,int v)
{
memset(InPath,false,sizeof(InPath));
while(true)
{
u = Base[u];
InPath[u] = true;
if(u == Start) break;
u = Father[Match[u]];
}
while(true)
{
v = Base[v];
if(InPath[v])break;
v = Father[Match[v]];
}
return v;
}
void ResetTrace(int u)
{
int v;
while(Base[u] != NewBase)
{
v = Match[u];
InBlossom[Base[u]] = InBlossom[Base[v]] = true;
u = Father[v];
if(Base[u] != NewBase) Father[u] = v;
}
}
void BloosomContract(int u,int v)
{
NewBase = FindCommonAncestor(u,v);
memset(InBlossom,false,sizeof(InBlossom));
ResetTrace(u);
ResetTrace(v);
if(Base[u] != NewBase) Father[u] = v;
if(Base[v] != NewBase) Father[v] = u;
for(int tu = 1; tu <= N; tu++)
if(InBlossom[Base[tu]])
{
Base[tu] = NewBase;
if(!InQueue[tu]) Push(tu);
}
}
void FindAugmentingPath()
{
memset(InQueue,false,sizeof(InQueue));
memset(Father,0,sizeof(Father));
for(int i = 1;i <= N;i++)
Base[i] = i;
Head = Tail = 1;
Push(Start);
Finish = 0;
while(Head < Tail)
{
int u = Pop();
for(int v = 1; v <= N; v++)
if(Graph[u][v] && (Base[u] != Base[v]) && (Match[u] != v))
{
if((v == Start) || ((Match[v] > 0) && Father[Match[v]] > 0))
BloosomContract(u,v);
else if(Father[v] == 0)
{
Father[v] = u;
if(Match[v] > 0)
Push(Match[v]);
else
{
Finish = v;
return;
}
}
}
}
}
void AugmentPath()
{
int u,v,w;
u = Finish;
while(u > 0)
{
v = Father[u];
w = Match[v];
Match[v] = u;
Match[u] = v;
u = w;
}
}
int Edmonds()
{
memset(Match,0,sizeof(Match));
for(int u = 1; u <= N; u++)
if(Match[u] == 0)
{
Start = u;
FindAugmentingPath();
if(Finish > 0)AugmentPath();
}
Count = 0;
for(int u = 1; u <= N;u++)
if(Match[u] > 0)
Count++;
return Count/2;
}
void PrintMatch()
{
//printf("%d\n",Count);
/*for(int u = 1; u <= N; u++)
if(u < Match[u]){
q[q_tot].x = u;
q[q_tot++].y = Match[u];
}
*/
int _ans = Edmonds();
// Debug(_ans);
vector<int> ans;
ans.clear();
for(int i = 1;i <= M; i++){
Memset(Graph, false);
for(int j = 1;j <= M; j++){
if(i != j){
if(p[i].x == p[j].y || p[i].y == p[j].x || p[i].x == p[j].x || p[i].y == p[j].y){
continue;
}
int a = p[j].x;
int b = p[j].y;
Graph[a][b] = Graph[b][a] = true;
}
}
int ts = Edmonds();
if(ts != _ans - 1){
ans.push_back(i);
}
}
int t = ans.size();
//Debug(t);
printf("%d\n", t);
sort(ans.begin(), ans.end());
if(t)
printf("%d", ans[0]);
for(int i = 1;i < t; i++){
printf(" %d", ans[i]);
}
printf("\n");
}
int main()
{
int u,v;
while(scanf("%d%d",&N,&M) != EOF){
memset(Graph,false,sizeof(Graph));
for(int i = 1;i <= M; i++){
scanf("%d%d",&u,&v);
Graph[u][v] = Graph[v][u] = true;
p[i].x = u;
p[i].y = v;
}
PrintMatch();//输出匹配数和匹配
}
return 0;
}
km算法 完美匹配
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 510,INF = 1e18;
ll n,m;
ll a[N][N];
ll ex[N],ey[N],visy[N],now;
ll match[N],pre[N],slack[N];
void bfs(ll u){
ll x,y = 0,yy = 0,d;
memset(pre,0,sizeof pre);
for(int i = 1; i <= n; i++) slack[i] = INF; //需要变小的点
match[y] = u;
while(1){
x = match[y],d = INF,visy[y] = now;
for(int i = 1; i <= n; i++){
if(visy[i] == now) continue;
if(slack[i] > ex[x] + ey[i] - a[x][i]){
slack[i] = ex[x] + ey[i] - a[x][i];
pre[i] = y; //表示 y对应的 点 y 需要减小的权值
}
if(slack[i] < d){
d = slack[i],yy = i; //找出减少最小的那条边
}
}
for(int i = 0; i <= n; i++){
if(visy[i] == now) ex[match[i]] -= d,ey[i] += d;
else slack[i] -= d;
}
y = yy;
if(match[y] == -1) break;
}
while(y){
match[y] = match[pre[y]],y = pre[y]; // 更新每个点对应的点
}
}
int main(){
scanf("%lld%lld",&n,&m);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
a[i][j] = -INF;
}
}
for(int i = 1; i <= m; i++){
ll x,y,w;
scanf("%lld%lld%lld",&x,&y,&w);
a[x][y] = max(a[x][y],w);
}
memset(match,-1,sizeof match);
memset(ex,0,sizeof ex);
memset(ey,0,sizeof ey);
memset(visy,0,sizeof visy);
for(int i = 1; i <= n; i++){
++now;
bfs(i);
}
ll sum = 0;
for(int i = 1; i <= n; i++){
if(match[i] != -1) sum += a[match[i]][i];
}
cout << sum << endl;
for(int i = 1; i <= n; i++){
cout << match[i] << " ";
}
return 0;
}