主頁 > 後端開發 > 圖論

圖論

2022-03-30 08:14:16 後端開發

圖論

圖論是數學的一個分支,它以圖為研究物件,圖論中的圖是由若干給定的點及連接兩點的線所構成的圖形,這種圖形通常用來描述某些事物之間的某種特定關系,用點代表事物,用連接兩點的線表示相應兩個事物間具有這種關系,

樹是一種資料結構,它是由n(n≥1)個有限節點組成一個具有層次關系的集合,把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的,它具有以下的特點:

其中每個元素稱為結點(node),每個節點有零個或多個子節點;沒有父節點的節點稱為根節點;每一個非根節點有且只有一個父節點;除了根節點外,每個子節點可以分為多個不相交的子樹,

如圖是一棵樹:

一棵樹中至少有1個結點,即根結點,

一個結點的子樹個數,稱為這個結點的度(如結點1的度為3,結點3的度為0),

度為0的結點稱為葉結點(leaf)(如結點3、5、6、8、9),

樹中各結點的度的最大值稱為這棵樹的度(此樹的度為3),

上端結點為下端結點的父結點,稱同一個父結點的多個子結點為兄弟結點(如結點1是結點2、3、4的父結點,結點 2、3、4是結點1的子結點,它們又是兄弟結點),

遍歷

樹結構解決問題時,按照某種次序獲得樹中全部結點的資訊,這種操作叫作樹的遍歷,

先序(根)遍歷

先訪問根結點,再從左到右按照先序思想遍歷各棵子樹(如,上圖先序遍歷的結果為125634789),

后序(根)遍歷

先從左到右遍歷各棵子樹,再訪問根結點(如,上圖后序遍歷的結果為562389741),

層次遍歷

按層次從小到大逐個訪問,同一層次按照從左到右的次序(如,上圖層次遍歷的結果為123456789),

葉結點遍歷

即從左到右遍歷所有葉節點(如,上圖葉節點遍歷的結果為56389),

二叉樹

二叉樹是一種特殊的樹型結構,它是度數為2的樹,即二叉樹的每個結點最多有兩個子結點,

每個結點的子結點分別稱為左兒子、右兒子,

五種基本形態

性質

性質一

二叉樹的第i層最多有2i-1個結點(i>=1)(可用二進制性質解釋,),

性質二

深度為k的二叉樹至多有2k–1個結點(k>=1),

性質三

任意一棵二叉樹,如果其葉結點數為n0,度為2的結點數為n2,則一定滿足:n0=n2+1,

性質四

有n個結點的完全二叉樹的深度為floor(log2n)+1,

性質五

一棵n個結點的完全二叉樹,對任一個結點(編號為i),有:如果i=1,則結點i為根,無父結點;如果i>1,則其父結點編號為floor(i/2),如果i為父節點編號,那么2i是左孩子,2i+1是右孩子,

圖A-滿二叉樹

圖B-完全二叉樹

編號示意圖

遍歷

二叉樹的遍歷是指按一定的規律和次序訪問樹中的各個結點,

遍歷一般按照從左到右的順序,共有3種遍歷方法,先(根)序遍歷,中(根)序遍歷,后(根)序遍歷,

先序遍歷

若二叉樹為空,則空操作,否則:

訪問根結點、先序遍歷左子樹、先序遍歷右子樹

void preorder(tree bt)//先序遞回演算法
{
	if(bt)
	{  
		cout << bt->data;
		preorder(bt->lchild);
		preorder(bt->rchild);
	}
}

先序遍歷此圖結果為:124753689

中序遍歷

若二叉樹為空,則空操作,否則:

中序遍歷左子樹、訪問根結點、中序遍歷右子樹

void inorder(tree bt)//中序遍歷遞回演算法
{
	if(bt)
	{  
		inorder(bt->lchild);
		cout << bt->data;
		inorder(bt->rchild);
	}
}

中序遍歷上圖結果為:742513869

后序遍歷

若二叉樹為空,則空操作,否則:

后序遍歷左子樹、后序遍歷右子樹、訪問根結點

void postorder(tree bt)//后序遞回演算法
{
	if(bt)
	{  
		postorder(bt->lchild);
		postorder(bt->rchild);
		cout << bt->data;
	}
}

后序遍歷上圖結果為:745289631

已知先序序列和中序序列可唯一確定一棵二叉樹;

已知中序序列和后序序列可唯一確定一棵二叉樹;

已知先序序列和后序序列不可唯一確定一棵二叉樹;

二叉樹操作(建樹、洗掉、輸出)

普通樹轉二叉樹

由于二叉樹是有序的,而且操作和應用更廣泛,所以在實際使用時,我們經常把普通樹轉換成二叉樹進行操作,

通用法則:“左孩子,右兄弟”

建樹

洗掉樹

插入一個結點到排序二叉樹中

在排序二叉樹中查找一個數

相關題目

擴展二叉樹

由于先序、中序和后序序列中的任一個都不能唯一確定一棵二叉樹,所以對二叉樹做如下處理,將二叉樹的空結點用“.”補齊,稱為原二叉樹的擴展二叉樹,擴展二叉樹的先序和后序序列能唯一確定其二叉樹,

現給出擴展二叉樹的先序序列,要求輸出其中序和后序序列,

輸入樣例:

ABD..EF..G..C..

輸出樣例:

DBFEGAC

DFGEBCA

二叉樹的建立和輸出

以二叉鏈表作存盤結構,建立一棵二叉樹,并輸出該二叉樹的先序、中序、后序遍歷序列、高度和結點總數,

輸入樣例:

12##3##

//#為空

輸出樣例:

123

//先序排列

213

//中序排列

231

//后序排列

2

//高度

3

//結點總數

因為本蒟蒻不太會用指標,所以自己寫了一個不帶指標的,代碼很丑,見諒QwQ

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;

int top,maxh;
char s;

struct t{
	int data,father,lson=0,rson=0,h=0;
	//這里的father其實并沒有用,但記錄一下可以更方便地找到一個點的父節點
}tree[100005];

void build(int father,bool right){
	cin>>s;
	if(s=='\n')
	return;
	if(s!='#'){
		++top;
		int t=top;
		tree[t].father=father;
		tree[t].data=https://www.cnblogs.com/audrey-hall/archive/2022/03/29/s-'0';
		tree[t].h=tree[father].h+1;
		maxh=max(tree[t].h,maxh);
		
		if(right==1)
		tree[father].rson=t;
		else
		tree[father].lson=t;
		
		build(t,0);
		build(t,1);
	}
	else return;
}

void xian(int now){
	cout<<tree[now].data;
	if(tree[now].lson!=0)
	xian(tree[now].lson);
	if(tree[now].rson!=0)
	xian(tree[now].rson);
}

void zhong(int now){
	if(tree[now].lson!=0)
	zhong(tree[now].lson);
	cout<<tree[now].data;
	if(tree[now].rson!=0)
	zhong(tree[now].rson);
}

void hou(int now){
	if(tree[now].lson!=0)
	hou(tree[now].lson);
	if(tree[now].rson!=0)
	hou(tree[now].rson);
	cout<<tree[now].data;
}

int main(){
	build(0,0);
//	for(int i=1;i<=top;i++){
//		cout<<tree[i].data<<' '<<tree[i].father<<' ';
//		cout<<tree[i].lson<<' '<<tree[i].rson<<' ';
//		cout<<tree[i].h<<endl;;
//	}
	xian(1);
	cout<<'\n';
	zhong(1);
	cout<<'\n';
	hou(1);
	cout<<'\n';
	cout<<maxh<<'\n'<<top<<'\n';
	return 0;
}

P1030 求先序排列

給出一棵二叉樹的中序與后序排列,求出它的先序排列,(約定樹結點用不同的大寫字母表示,長度<=8),

輸入:

2行,均為大寫字母組成的字串,表示一棵二叉樹的中序與 后序排列,

輸出:

1行,表示一棵二叉樹的先序,

輸入樣例:

BADC

BDCA

輸出樣例:

ABCD

分析

中序為BADC,后序為BDCA,所以A為根結點,B、DC分別為左右子樹的中序序列,B、DC分別為左右子樹的后序序列,然后再遞回處理中序為B,后序為B的子樹和中序為DC,后序為DC的子樹,

自己用char陣列寫的代碼QwQ

#include<iostream>
#include<cstring>
#include<cstdio>
#define ll long long
using namespace std;
char mid[10],post[10];
//mid陣列記錄中序排列,post陣列記錄后序排列
//除了打暴力最好不要用string
int z[10],m[10],p[10];
//z陣列做中轉使用,m陣列記錄mid陣列的內容,p陣列記錄post陣列的每一位在mid陣列中的位置
void find(int start,int end,int kai,int jie){
//start和end記錄我們正在找的mid陣列的范圍
//kai(開頭)和jie(結尾)記錄我們正在找的post陣列的范圍
    if(start>end||kai>jie)return;
    //如果開頭大于結尾,就回傳
    if(start==end||kai==jie){
        printf("%c",mid[p[jie]]);
        return;
    }
    //如果開頭等于結尾,那此節點一定沒有兒子,輸出當前節點并回傳
    printf("%c",mid[p[jie]]); 
    //前面說過后序排列的最后一位就是當前樹的根節點,所以p[jie]就是根節點在mid陣列中的位置
    //開頭小于結尾,那就輸出當前節點然后再去尋找此節點的左兒子和右兒子
    find(start,p[jie]-1,kai,kai+p[jie]-start-1);
    //求左子樹的范圍,然后遞回尋找左兒子
    find(p[jie]+1,end,kai+p[jie]-start,jie-1);
    //求右子樹的范圍,然后遞回尋找右兒子
}
int main(){
    scanf("%s%s",mid+1,post+1);
    //輸入時下標從1開始(主要是因為我比較毛病)
    int len=strlen(mid+1);
    //輸入時下標從1開始那么計算字串長度時也要加1
    for(int i=1;i<=len;i++){
        m[i]=mid[i]-'A'+1;
        //將每一位轉成數字以方便處理(是的,我很毛病)
        z[m[i]]=i;
        //z陣列記錄m陣列每一位的位置(這一步是為了方便后面記錄post數字)
    }
    for(int i=1;i<=len;i++){
        p[i]=z[post[i]-'A'+1];
        //記錄post陣列的每一位在mid陣列中的位置
        //z:我滴任務完成啦!
    }
    find(1,len,1,len);
    //開始遞回
    return 0;
}

求后序排列

輸入:

二叉樹的前序序列與中序序列

輸出:

二叉樹的后序序列

樣例輸入:

abcdefg

cbdafeg

樣例輸出:

cdbfgea

#include<iostream>
#include<cstring>
#include<cstdio>
#define ll long long
using namespace std;
char qian[100005],zhong[100005];
int q[100005],z[100005],a[100005],cnt=0;
void find(int start,int end){
	if(start>end){
		return;
	}
	cnt++;
	if(start==end){
		cout<<char(z[q[cnt]]+'a'-1);
		return;
	}
	int t=cnt;
	find(start,q[t]-1);
	find(q[t]+1,end);
	cout<<char(z[q[t]]+'a'-1);
}
int main(){
	cin>>qian>>zhong;
	int len=strlen(qian);
	for(int i=0;i<len;i++){
		a[zhong[i]-'a']=i;
	}
	for(int i=0;i<len;i++){
		z[i+1]=zhong[i]-'a'+1;
		q[i+1]=a[qian[i]-'a']+1;
	}
	find(1,strlen(qian));
	return 0;
}

因為有小可愛說我的代碼在輸入時的處理不清楚,所以又寫了一個版本QwQ

#include<iostream>
#include<cstring>
#include<cstdio>
#define ll long long
using namespace std;
char qian[100005],zhong[100005];
int q[100005],z[100005],a[100005],cnt=0;
void find(int start,int end){
//	cout<<endl<<'*'<<start<<' '<<end<<'*'<<endl;
	if(start>end){
		return;
	}
	cnt++;
	if(start==end){
		cout<<char(z[q[cnt]]+'a'-1);
		return;
	}
	int t=cnt;
	find(start,q[t]-1);
	find(q[t]+1,end);
	cout<<char(z[q[t]]+'a'-1);
}
int main(){
//	cin>>qian+1>>zhong+1;
	scanf("%s%s",qian+1,zhong+1);//這里的輸入下標從1開始
	int len=strlen(qian+1);
	for(int i=1;i<=len;i++){
		a[zhong[i]-'a']=i;
	}
	for(int i=1;i<=len;i++){
		z[i]=zhong[i]-'a'+1;
		q[i]=a[qian[i]-'a'];
	}
	find(1,len);
	return 0;
}

運算式樹

關于運算式樹,我們可以分別用先序、中序、后序的遍歷方法得出完全不同的遍歷結果,如,對于下圖的遍歷結果如下,它們對應著運算式的3種表示方法,

-+a*b-cd/ef  (前綴表示、波蘭式)

a+b*(c-d)-e/f  (中綴表示)

abcd-*+ef/-  (后綴表示、逆波蘭式)

哈夫曼樹

QwQ,不是很會,那就推薦一篇博客吧,

前置知識

如果資料元素集合中的各元素之間存在任意的關系,則此資料結構稱為圖,

如果將資料元素抽象為頂點(V),元素之間的關系用邊(E)表示,則圖亦可以表示為G=(V,E),其中V是頂點的有窮(非空)集合,E為邊的集合,

邊權

離散數學或資料結構中,圖的每條邊上帶的一個數值,它代表的含義可以是長度等等,這個值就是邊權,

頂點的度

與該頂點相關聯的邊的數目,有奇點、偶點之分,

入度(有向圖)

該頂點的入邊的數目,

出度(有向圖)

該頂點的出邊的數目,

補充:

一個圖中,全部頂點的度數之和為所有邊數的2倍;

有向圖中所有頂點的入度之和等于所有頂點的出度之和;

任意一個無向圖一定有偶數個奇點,

子圖

設兩個圖G=(V,E)和G’=(V’,E’),若V’是V的子集,且E’是E的子集,則稱G’是G的子圖,

路徑、簡單路徑、連通集

對于圖G=(V,E),對于頂點a、b,如果存在一些頂點序列x1=a,x2,……,xk=b(k>1),且(xi,xi+1)∈E,i=1,2…k-1,則稱頂點序列x1,x2,……,xk為頂點a到頂點b的一條路徑,而路徑上邊的數目(即k-1)稱為該路徑的長度,并稱頂點集合{x1,x2,……,xk}為一個連通集,

如果一條路徑上的頂點除了起點和終點可以相同外,其它頂點均不相同,則稱此路徑為一條簡單路徑,

回路

起點和終點相同的簡單路徑稱為回路(或環),

連通

在一個圖中,如果從頂點U到頂點V有路徑,則稱U和V是連通的,

連通圖

如果一個無向圖中,任意兩個頂點之間都是連通的,則稱該無向圖為連通圖,否則稱為非連通圖,

連通分量

一個無向圖的連通分量定義為該圖的最大連通子圖,

補充:

任何連通圖的連通分量只有一個,即本身,而非連通圖有多個連通分量,

強連通圖

在一個有向圖中,對于任意兩個頂點U和V,都存在著一條從U到V的有向路徑,同時也存在著一條從V到U的有向路徑,則稱該有向圖為強連通圖,

強連通分量

一個有向圖的強連通分量定義為該圖的最大的強連通子圖,

補充:

強連通圖只有一個強連通分量,即本身,非強連通圖有多個強連通分量,

圖的連通性判斷(用BFS和DFS實作)

分類

無向圖

邊集E(G)中為無向邊,

有向圖

邊集E(G)中為有向邊,

帶權圖

邊上帶有權的圖,也稱為網,(又分有向帶權圖、無向帶權圖)

完全圖

若是無向圖,則每兩個頂點之間都存在著一條邊;若是有向圖,則每兩個頂點之間都存在著方向相反的兩條邊,

補充:

一個n個頂點的完全無向圖含有n×(n-1)/2條邊;

一個n個頂點的完全有向圖含有n×(n-1)條邊,

稠密圖

邊數接近完全圖的圖,

稀疏圖

邊數遠遠少于完全圖的圖,

存盤

圖型結構的存盤分為靜態存盤和動態存盤,

鄰接矩陣

鄰接矩陣是表示頂點間相鄰關系的矩陣,若G=(V,E)是一個具有n個頂點的圖,則G的鄰接矩陣是如下定義的二維陣列a,其規模為n*n,

a[i,j]={1(或權),(vi,vj)∈E;
0(±∞),(vi,vj)?E}

//第8行將每一個點初始化為無窮大,表示不聯通,

//如果圖不帶權,可以用g[i][j]=0表示不連通,

#include<iostream>
using namespace std;

double g[101][101];//全為0,不通

int main(){
	cin>>n;
	//鄰接矩陣存盤 
    for(int i=1;i<=n;i++)
    	for(int j=1;j<=n;j++)
	    	cin>>g[i][j];
    for(int i=1;i<=n;i++){ 
		int tot=0; 
		//統計每行數字1,即出度
    	for(int j=1;j<=n;j++)
	    	if(g[i][j]>0)tot++;
	    	a[i]=tot; //按行統計存盤 
	}
	...... 
	return 0; 	
}

特點

占用的存盤單元數只與頂點數n有關,與邊數無關,n*n的二維陣列,

方便度數的計算,

容易判斷兩點之間是否有邊相連,

尋找一個點相連的所有邊需要一個1到n的回圈,

鄰接表(用陣列+結構體模擬)

方法一

定義二維陣列g[101][101],g[i][0]表示i發出的邊的數量,g[i][j]表示i發出的第j條邊指向哪個頂點,

這樣就可以處理i發出的每條邊,也就能找到頂點i指向的頂點,

方法二

#include<iostream>
using namespace std;

const int maxn=1001,maxm=100001;
int head[maxn],num_edge,n,m,u,v;
struct Edge{	
	int next;//下一條邊的編號
	int to;//這條邊到達的點
}edge[maxm];//結構體變數

void add_edge(int from,int to){ 
//加入一條從from到to的單向邊
	edge[++num_edge].next=head[from];
	edge[num_edge].to=to;
	head[from]=num_edge;
}

int main(){  
	num_edge=0;
	scanf("%d %d",&n,&m);//讀入點數和邊數
	for(int i=1;i<=m;i++){
	scanf("%d %d",&u,&v);//u、v之間有一條邊
    add_edge(u,v);
    }
    int j,chudu[maxn];
    for(int i=0;i<n;i++){
	//求出每一個頂點的出度
        int tot=0;
        j=head[i];
        while(j!=0){
            tot++;
        	j=edge[j].next;
        }
        chudu[i]=tot;
	}
    ...... 
	return 0;
}

特點

適用于點多邊少的稀疏圖,

(對于有n個點,m條邊的稀疏圖來說,用鄰接矩陣存會開n2的空間;而鄰接表則是視邊數的多少來開記憶體大小)

可以快速找到與當前頂點相連的點,

(結構體的next指標比較方便)

判斷兩點是否相連不如鄰接矩陣快速,

(鄰接矩陣是看aij的數值,直接O(1)查詢即可;鄰接表判斷起來比較繁瑣,)

邊集陣列

是利用一維陣列存盤圖中所有邊的一種圖的表示方法,

邊集陣列由兩個一維陣列構成,一個存盤頂點的資訊,另一個存盤邊的資訊,這個邊陣列每個資料元素由一條邊的起點下標(begin),終點下標(end)和權(weight)組成,

前向星

以儲存邊的方式來存盤圖,通常用在點的數目太多,或兩點之間有多潭訓的時候,一般在別的資料結構不能使用的時候才考慮用前向星,除了不能直接用起點終點定位以外,前向星幾乎是完美的,

實作

讀入每條邊的資訊,將邊存放在陣列中,把陣列中的邊按照起點順序排序(可以使用基數排序),前向星就構造完了,

#include<bits/stdc++.h>
using namespace std;
struct Node{
	int v,next;
}E[100001];
int p[100001],eid=0;
inline void insert(int u,int v){
    eid++;
    E[eid].v=v;
    E[eid].next=p[u];
    p[u]=eid;
}

遍歷

從圖中某一頂點出發系統地訪問圖中所有頂點,使每個頂點恰好被訪問一次,這種運算操作被稱為圖的遍歷,

為避免重復訪問,需要一個狀態陣列vis[n],用來存盤各頂點的訪問狀態,如果vis[i]=1,則表示頂點i已經訪問過;如果vis[i]=0,則表示頂點i還未訪問過,初始化時,各頂點的訪問狀態均為0,

深度優先遍歷(dfs)

#include<iostream>
using namespace std;
int n,m;
int a[100][100];
int vis[100];//標記陣列
void dfs(int u){   
	cout<<"V"<<u<<" ";
	vis[u]=1;//訪問標記 
	for(int i=1;i<=n;i++)
		if(a[u][i]==1&&vis[i]==0)
	    	dfs(i); 
}
int main(){
	cin>>n; //鄰接矩陣存盤 
   	for(int i=1;i<=n;i++)
      	for(int j=1;j<=n;j++)
	     	cin>>a[i][j];
    dfs(1);//選定V1開始dfs遍歷,
	return 0; 	
}

廣度優先遍歷(bfs)

為了實作逐層訪問,bfs演算法在實作時需要使用一個佇列,

補充

//如果是非連通圖,主程式做如下修改:
int main(){
	...
	memset(vis,0,sizeof(vis));
    //把各個點全掃一遍
    for(int i=1;i<=n;i++)
        if(vis[i]==0)dfs(i);
	...
   	return 0;
}

AOV網

在日常生活中,一項大的工程可以看作是由若干個子工程(這些子工程稱為“活動”)組成的集合,

這些子工程(活動)之間必定存在一些先后關系,即某些子工程(活動)必須在其它一些子工程(活動)完成之后才能開始,我們可以用有向圖來形象地表示這些子工程(活動)之間的先后關系,

子工程(活動)為頂點,子工程(活動)之間的先后關系為有向邊,這種有向圖稱為“頂點活動網路”,又稱“AOV網”,

在AOV網中,有向邊代表子工程(活動)的先后關系,我們把一條有向邊起點的活動稱為終點活動的前驅活動,同理終點的活動稱為起點活動的后繼活動,

而只有當一個活動全部的前驅全部都完成之后,這個活動才能進行,

一個AOV網必定是一個有向無環圖,即不應該帶有回路,否則,會出現先后關系的自相矛盾,

拓撲排序

所謂拓撲排序,就是把AOV網中的所有活動排成一個序列, 使得每個活動的所有前驅活動都排在該活動的前面,這個程序稱為“拓撲排序”,所得到的活動序列稱為“拓撲序列”,

即把有向圖上的n個點重新標號為1到n,滿足對于任意一條邊 (u, v),都有u<v,

并不是所有的圖都能進行拓撲排序,只要圖中有環,那么就可以匯出矛盾,

因此拓撲排序演算法只適用于AOV網(有向無環圖,DAG),有向無環圖有很多優美的性質,比如可以在拓撲序上進行DP,

一個AOV網的拓撲序列不一定是唯一的,

實作

我們記錄一下每一個點的入度和出度,用一個佇列維護當前所有入度為0的點,每次拿出來一個入度為0的點并且將它加到拓撲序中,然后列舉出邊更新度數上述兩步,重復,直到不存在入度為0的頂點為止,

若完成時佇列中的頂點數小于AOV網中的頂點數,則圖中有回路,否則此佇列中的頂點序列就是一種拓撲序,

可以看出,拓撲排序可以用來判斷一個有向圖是否有環,因為只有有向無環圖才存在拓撲序列,

時間復雜度O(n+m),

(在拓撲排序的程序中可以順帶進行DP)

思路

indgr[i]:頂點i的入度;

stack[ ]:堆疊;

初始化:top=0 (堆疊頂指標置零);

將初始狀態所有入度為0的頂點壓堆疊;

I=0 (計數器);

while 堆疊非空(top>0)

堆疊頂的頂點v出堆疊;top-1; 輸出v;i++;

for v的每一個后繼頂點u

ndgr[u]--;//u的入度減1

if (u的入度變為0) 頂點u入堆疊

演算法結束

代碼

	for(int i=1;i<=n;i++)
		if(d[i]==0)q.push(i);
	while(!q.empty()){
		int node=q.front();
		q.pop();
		res[++top]=node;
		for(int hd=head[node];hd;hd=e[hd].nxt){
			d[e[hd].to]--;
			if(d[e[hd].to]==0)
			q.push(e[hd].to);
		}
	}

歐拉路徑(歐拉通路)

如果圖中的一個路徑包括每個邊恰好一次,則該路徑稱為歐拉路徑,

歐拉回路

首尾相接的歐拉路徑稱為歐拉回路,

判定

由于每一條邊都要經過恰好一次,因此對于除了起點和終點之外的任意一個節點,只要進來,一定要出去,

一個無向圖存在歐拉回路,當且僅當該圖所有頂點度數都為偶數(即不存在奇點),且該圖只有一個存在邊的連通塊,

一個無向圖存在歐拉路徑,當且僅當該圖中奇點的數量為0或2,且該圖只有一個存在邊的連通塊;
如果存在2個奇點,則此歐拉路徑一定是從一個奇點出發,在另一個奇點結束,

一個有向圖存在歐拉回路,當且僅當所有點的入度等于出度,

一個混合圖存在歐拉回路,當且僅當存在一個對所有無向邊定向的方案,使得所有點的入度等于出度,需要用網路流,

求法

我們用 dfs來求出一張圖的歐拉回路,

我們給每一條邊一個 vis陣列代表是否訪問過,接下來從一個點出發,遍歷所有的邊,

直接dfs并且記錄的話會有一些問題,

為了解決這個問題,我們在記錄答案的時候倒著記錄,也就是當我們通過 (u, v) 這條邊到達 v 的時候,先把 v dfs 完再加入 (v, u) 這條邊,

還有一點需要注意,因為一個點可能被訪問多次,一不小心可能會寫成 O(n 2 ) 的(因為每次遍歷所有的出邊),解決方案就是設一個cur陣列,每次直接從上一次訪問到的出邊繼續遍歷,

時間復雜度 O(n + m),

代碼

	void dfs(int x)
	{
		for(int&hd=head[x];hd;hd=e[hd].nxt)
		{
			if(flag[hd>>1])continue;
			flag[hd>>1]=1;
			dfs(e[hd].to);
			a[++top]=x;
 		}
	}

歐拉圖

有歐拉回路的圖稱為歐拉圖(Euler Graph);

有歐拉通路而沒有歐拉回路的圖稱為半歐拉圖,

哈密爾頓通路

通過圖中每個頂點一次且僅一次的通路,

哈密爾頓回路

通過圖中每個頂點一次且僅一次的回路,

求法

一般用搜索解決,

哈密爾頓圖

存在哈密爾頓回路的圖,

最短路徑

所謂最短路,就是把邊權看做邊的長度,從某個點 S到另一個點 T 的最短路徑,

用更加數學化的語言描述就是,對于映射 $ f : V \to R $ ,滿足 $ f \left ( S \right ) = 0 $ 且 $ \forall ( x , y , l ) \in E $ , | f ( x ) ? f ( y ) | $ \le l $ 的情況下,f(T) 的最大值,

最短路問題

最短路問題(short-path problem)是網路理論解決的典型問題之一,可用來解決管路鋪設、線路安裝、廠區布局和設備更新等實際問題,

基本內容是:若網路中的每條邊都有一個數值(長度、成本、時間等),則找出兩節點(通常是源節點和終節點)之間總權和最小的路徑,也就是最短路問題,

一般有兩類最短路徑問題:一類是求從某個頂點(源點)到其它頂點(終點)的最短路徑,(Dijkstra、Bellman-ford、SPFA);另一類是求圖中每一對頂點間的最短路徑,(floyed),

單源最短路——Dijkstra

在所有的邊權均為正的情況下,我們可以使用Dijkstra演算法求出一個點到所有其它點的最短路徑,

我們維護一個集合,表示這個集合內的點最短路徑已經確定了,

每次我們從剩下的點中選擇當前距離最小的點 u 加入這個集合,然后列舉另一個點 v 進行更新:

$ Dv = min \left ( Dv , Du + W \left ( u , v \right ) \right ) $

直接這樣做時間復雜度是 $ O \left ( n^2 \right ) $ 的,

實作

設起點為s,dis[ v ]表示從s到v的最短路徑,path[ v ]為v的前驅節點,用來輸出路徑,

1、初始化:

$ dis \left [ v \right ] = + \infty \left ( v \ne s \right ) ; $

$ dis \left [ s \right ] = 0 ; $

$ path \left [ s \right ] = 0 ; $

2、在沒有被訪問過的點中找一個頂點u使得dis[ u ]是最小的,

3、u標記為已確定最短路徑,

4、for與u相連的每個未確定最短路徑的頂點v,

#include<iostream>	
#include<cstring>   
using namespace std;   
const int inf=99999; 
const int maxn=101;
int f[maxn],a[maxn][maxn],d[maxn],path[maxn];  
int n,start; 
void init(){
	cin>>n>>m>>start;
	int x,y,w;
	for(int i=1;i<=n;i++)
	  	for(int j=1;j<=n;j++){
	  		if(j==i) a[i][j]=0;
	  		else a[i][j]=inf;
	  	}
	for(int i=1;i<=m;i++){        
		cin>>x>>y>>w;
		a[x][y]=w;
		a[y][x]=w;
	}
}
void dijkstra(int s){
	for(int i=1;i<=n;i++){
		d[i]=inf; 
		f[i]=false;  
	}
    d[s]=0;
    for(int i=1;i<=n;i++){
		int mind=inf;  
		int k;//用來記錄準備放入集合1的點
		for(int j=1;j<=n;j++)//查找集合2中d[]最小的點
		if((!f[j])&&(d[j]<mind)){ 
			mind=d[j]; 
			k=j; 
		}
		if(mind==inf)break;//更新結點求完了
		f[k]=true;//加入集合1
		for(int j=1;j<=n;j++)//修改集合2中的d[j]
			if((!f[j])&&(d[k]+a[k][j]<d[j])){ 
				d[j]=d[k]+a[k][j];
				path[j]=k;
			}
    } 
}
void dfs(int i){
	if(i!=start)dfs(path[i]);
	cout<<i<<' ';
}
void write(){
    cout<<start<<"到其余各點的最短距離是:"<<endl; 
    for(int i=1;i<=n;i++){
		if(i!=start){
	    	if(d[i]==inf)cout<<i<<"不可達";
	    	else{
				cout<<i<<"的最短距離:"<<d[i]<<",依次經過的點是:";
				dfs(path[i]);
				cout<<i<<endl;		 
	        } 
	    }		
    }
}
int main(){
	init();
	dijkstra(start);
	write();
	return 0; 
}

優化

我們注意到,復雜度主要來源于兩個地方,

第一個是找出當前距離最小的點,這個可以用堆很容易地實作,

第二個是列舉 v,如果我們用鄰接表存圖,可以降到邊數級別,

這樣我們就把復雜度降到了 O((n + m) log n),

單源最短路——Bellman-Ford

簡稱Ford(福特)演算法,另一種求單源最短路的演算法,復雜度不如Dijkstra優秀,能夠處理存在負邊權的情況,但無法處理存在負權回路的情況,

負權回路

存在負權回路的圖無法求出最短路徑,Bellman-Ford演算法可以在有負權回路的情況下輸出錯誤提示,

如果在Bellman-Ford演算法的兩重回圈完成后,還是存在某條邊使得: $ dis \left [ u \right ] + w \left [ j \right ] < dis \left [ v \right ] $ ,則存在負權回路,

if(dis[u]+w[j]<dis[v])return false;

實作

設s為起點,dis[ v ]即為s到v的最短距離,pre[v]為v前驅,w[ j ]是邊j的長度,且j連接u、v,

初始化:

$ dis \left [ v \right ] = + \infty \left ( v \ne s \right ) ; $

$ dis \left [ s \right ] = 0 ; $

$ path \left [ s \right ] = 0 ; $

考慮在上面出現過的松弛操作:

dv = min(dv, du + w(u, v))

由于最短路徑只會經過最多 n 個點,因此每一個點的最短路徑只會被松弛至多 n ? 1 次,

所以我們可以對整張圖進行 n ? 1 次松弛操作,每次列舉所有的邊進行更新,

for(i=1;i<=n-1;i++)
	for(j=1;j<=E;j++)//列舉所有邊,而不是列舉點
		if(dis[u]+w[j]<dis[v]){//u、v分別是這條邊連接的兩個點,
     	  	dis[v] =dis[u] + w[j];
       		pre[v] = u;
  		}

時間復雜度 O(nm),

SPFA

它死了,(因為它的復雜度是錯誤的)

應用:費用流

Bellman-Ford演算法不夠優秀,于是我們嘗試改進這個演算法,

注意到,在進行松弛操作的時候,如果點 u 的距離一直沒有發生變化,那么就不需要再列舉這個點的出邊進行松弛了,

也就是說我們可以用一個佇列保存所有距離發生變化的點, 每次取出一個點進行更新,

(主要思想:初始時將起點加入佇列,每次從佇列中取出一個元素,并對所有與它相鄰的點進行修改,若某個相鄰的點修改成功,則將其入隊,直到佇列為空時演算法結束,)

于是SPFA就誕生了,

如果圖是隨機的,SPFA的期望時間復雜度約為 O(2m),比之前提到的任何一個演算法都優秀,而且還可以有負權,

但是在最壞情況下它的復雜度和 Bellman-Ford 相同,都是 O(nm),在正式比賽中,沒有哪個出題人會放過它,(因為其復雜度本來就是錯的)

多源最短路——Floyed

對于一張圖,我們希望求出任意兩個點之間的最短路徑,

我們用 DP(動態規劃) 的思想,設 fi,j,k 表示從 i 到 j,途中僅經過前 k個點的最短路,

由于每一個點在最短路中只會出現一次(不然就出現負環了,不存在最短路),所以可以很寫出轉移方程:

fi,j,k = min(fi,j,k?1, fi,k,k?1 + fk,j,k?1)

時間復雜度是 $ O \left ( n^3 \right ) $ ,

在實際求的程序中,最后一維可以用滾動陣列優化掉,所以空間復雜度是 $ O \left ( n^2 \right ) $ ,

代碼

for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);

注意三層回圈的順序不能顛倒,

初始化

$ d \left [ i \right ] \left [ i \right ] = 0 $ //自己到自己為0;

$ d \left [ i \right ] \left [ i \right ] = 邊權 $ //i與j有直接相連的邊;

$ d \left [ i \right ] \left [ i \right ] = + \infty $ //i與j無直接相連的邊,

// 如果是int陣列,采用memset(d, 0x7f, sizeof(d))可全部初始化為一個很大的數

Floyd 傳遞閉包

有時候,我們需要維護一些有傳遞性的關系,比如相等,連通等等,(12連通,23連通,則 13連通)

初始條件往往是已知若干個點對具有這些關系,然后讓你弄 出來所有的關系,

可以直接把 Floyd 演算法做一下調整——

dis[i][j]=dis[i][j]|(dis[i][k]&dis[k][j]);

這個演算法叫做傳遞閉包,

多源最短路——Johnson 重賦權

對于多源最短路,如果我們列舉一個點然后跑堆優化的 Dijkstra,那么復雜度是 O(nm log n) 的,在圖比較稀疏的情況下,這個復雜度要優于 Floyd 演算法的 O(n 3 ),

但是 Dijkstra 演算法要求所有邊權均非負,

于是就有了重賦權的技巧,

我們新建一個 0 號點,并且從這個點出發向所有點連一條邊 權為 0 的邊,然后跑單源最短路,(SPFA 或者 Bellman-Ford)

設距離陣列為 h,接下來對于每條邊 (u, v),令 w ′ (u, v) = w(u, v) + h(u) ? h(v),

這樣所有的邊權就都變成非負了,我們就可以跑 Dijkstra 演算法了,

證明

首先由于 h(v) ≤ h(u) + w(u, v),新圖的邊權一定非負,

設新圖上的最短路徑為 d ′,原圖上的最短路徑為 d,

d ′ (u, v) = min a1,a2,...,ak w ′ (u, a1) + w ′ (a1, a2) + · · · + w ′ (ak, v)

= min a1,a2,...,ak w(u, a1) + (h(u) ? h(a1)) + w(a1, a2)+ (h(a2) ? h(a1)) + · · · + w(ak, v) + (h(v) ? h(ak))

= h(u) ? h(v) + min a1,a2,...,ak w(u, a1) + · · · + w(ak, v)

= h(u) ? h(v) + d(u, v)

最短路樹(最短路圖)

所謂最短路樹,就是在求完從 S 出發的單源最短路之后,

只保留最短路上的邊形成的資料結構,

只需要在求的程序中維護一個pre陣串列示這個點的前驅即可,很多最短路的變種都需要用這個演算法,

最小生成樹

用來解決如何用最小的代價用N-1條邊連接N個點的問題,

Prim 演算法

類比 Dijkstra 演算法,我們維護一個集合 S,表示這個集合中 的生成樹已經確定了,

演算法流程和Dijkstra一樣,唯一的區別是用w(u, v) 去更新 dv 而不是用 du + w(u, v),

時間復雜度 $ O \left ( n^2 \right ) $ ,同樣可以用堆優化,

主要思想

Prim演算法采用“藍白點”思想:白點代表已經進入最小生成樹的點,藍點代表未進入最小生成樹的點,

Prim演算法每次回圈都將一個藍點u變為白點,并且此藍點u與白點相連的最小邊權min[u]還是當前所有藍點中最小的,

這樣相當于向生成樹中添加了n-1次最小的邊,最后得到的一定是最小生成樹,

n次回圈,每次回圈讓一個新的點加入生成樹,n次回圈就能把所有點囊括到其中;每次回圈我們都能讓一條新的邊加入生成樹,n-1次回圈就能生成一棵含有n個點的樹;每次回圈我們都取一條最小的邊加入生成樹,n-1次回圈結束后,我們得到的就是一棵最小的生成樹,

實作

以第一個點為起點生成最小生成樹,min[ v ]表示藍點v與白點相連的最小邊權,

MST表示最小生成樹的權值之和,

初始化:

$ min \left [ v \right ] = + \infty \left ( v \ne 1 \right ) ; $

$ min \left [ 1 \right ] = 0 ; $

$ MST = 0 ; $

部分偽代碼:

for (i = 1; i<= n; i++)
	尋找min[u]最小的藍點u,
	將u標記為白點
	MST+=min[u]
	for 與白點u相連的所有藍點v  
   		if (w[u][v]<min[v]) 
   		min[v]=w[u][v];

演算法結束:

MST即為最小生成樹的權值之和

Kruskal 演算法

前置知識

并查集演算法

并查集主要用于解決一些元素分組的問題,它管理一系列不相交的集合,并支持兩種操作:

合并:把兩個不相交的集合合并為一個集合,

查詢:查詢兩個元素是否在同一個集合中,

主要思想

Kruskal演算法將一個連通塊當做一個集合,

Kruskal首先將所有的邊按從小到大順序排序(快排),并認為每一個點都是孤立的,分屬于n個獨立的集合,

然后按順序列舉每一條邊,如果這條邊連接著兩個不同的集合,那么就把這條邊加入最小生成樹,這兩個不同的集合就合并成了一個集合;如果這條邊連接的兩個點屬于同一集合,就跳過,

直到選取到第n-1條邊為止,

因為是求的最小生成樹,所以我們用貪心的思路,把所有的邊權從小到大排序,然后一條一條嘗試加入,用并查集維護連通性,

可以發現這樣一定能得到原圖的最小生成樹,

時間復雜度$ O \left ( m log m \right ) $ ,

實作

初始化并查集:

$ father \left [ x \right ] = x $

$ tot = 0 $

將所有邊用快排從小到大排序,

計數器 k=0;

for(i=1;i<=M;i++)//遍歷所有邊 
  	if(這是一條u,v不屬于同一集合的邊(u,v)(因為已經排序,所以必為最小)){
		合并u,v所在的集合
		把邊(u,v)加入最小生成樹
     	tot=tot+W(u,v);
    	k++;
      	if(k=n-1)//說明最小生成樹已經生成
			break; 
	} 

結束,tot即為最小生成樹的總權值之和,

代碼

find(int x){
	return x==pa[x]?x:pa[x]=find(pa[x])
}

證明

如果某一條邊 (u, v) 不屬于最小生成樹,那么考慮最小生成樹上 連接 u, v 的路徑,這上面一定有一條邊權不小于 w(u, v) 的邊 (因為我們是從小到大列舉的所有邊),這樣替換后答案一定不會變劣,

優化

路徑壓縮(O(logn))+安值合并(O(logn))→O(αn)(αn在 $ 10^8 $ 資料內不超過4,可視為常數)

Kruskal 重構樹

前置知識

dfs(深度優先搜索)

LCA(最近公共祖先)

在一棵沒有環的樹上,每個節點肯定有其父親節點和祖先節點,而最近公共祖先,就是兩個節點在這棵樹上深度最大的公共的祖先節點,

LCA主要是用來處理當兩個點僅有唯一一條確定的最短路徑時的路徑,

樹上倍增

用于求LCA(最近公共祖先),

倍增的思想是二進制,

首先開一個n×logn的陣列,比如fa[n][logn],其中fa[i][j]表示i節點的第2^j個父親是誰,

然后,我們會發現一個性質:

fa[i][j]=fa[fa[i][j-1]][j-1]

用文字敘述為:i的第2^j個父親 是i的第2(j-1)個父親的第2(j-1)個父親,

這樣,本來我們求i的第k個父親的復雜度是O(k),現在復雜度變成了O(logk),

Kruskal 重構樹是基于 Kruskal 最小生成樹演算法的一種算 法,它主要通過將邊權轉化為點權來實作,

流程

將所有邊按照邊權排序,設 r(x) 表示 x 所在連通塊的根節點,(注意這里要用并查集)

列舉所有的邊 (u, v),若 u, v 不連通,則新建一個點 x,令 x 的權值為 w(u, v), 連接 (x, r(u)) 和 (x, r(v)), 令 r(u) = r(v) = x,

不斷重復以上程序,直到所有點均連通,

時間復雜度 O(m log m),

性質

這樣,我們就得到了一棵有 2n ? 1 個節點的二叉樹,其中葉節點為原圖中的點,其余的點代表原圖中的邊,并且滿足父節點權值大于等于子節點,

它有什么用呢?

求 u, v 之間路徑上的最大邊權 → 求重構樹上 u, v 兩個點的 LCA,

只保留邊權小于等于 x 的邊形成的樹 → 重構樹上點權小于 等于 x 的點的子樹,

Bor?vka 演算法

前置知識

距離

(x1,y1)(x2,y2)

曼哈頓距離:|x1-x2|+|y1-y2|

切比雪夫距離:max(|x1-x2|,|y1-y2|

歐幾里得距離:√[(x1-x2)2+(y1-y2)2]

曼哈頓距離與切比雪夫距離的相互轉化

兩者之間的關系

我們考慮最簡單的情況,在一個二維坐標系中,設原點為(0,0),

如果用曼哈頓距離表示,則與原點距離為1的點會構成一個邊長為2–√2的正方形,

如果用切比雪夫距離表示,則與原點距離為1的點會構成一個邊長為2的正方形,

對比這兩個圖形,我們會發現這兩個圖形長得差不多,他們應該可以通過某種變換互相轉化,

事實上,

將一個點(x,y)的坐標變為(x+y,x?y)后,原坐標系中的曼哈頓距離 =新坐標系中的切比雪夫距離,

將一個點(x,y)的坐標變為((x+y)/2,(x?y)/2) 后,原坐標系中的切比雪夫距離 = 新坐標系中的曼哈頓距離,

(注意:切比雪夫距離轉曼哈頓距離要再除以二)

用處

切比雪夫距離在計算的時候需要取max,往往不是很好優化,對于一個點,計算其他點到該的距離的復雜度為O(n),

而曼哈頓距離只有求和以及取絕對值兩種運算,我們把坐標排序后可以去掉絕對值的影響,進而用前綴和優化,可以把復雜度降為O(1),

第三種求最小生成樹的演算法,雖然比較冷門但是很多題需要用到這個演算法,

我們維護當前形成的所有連通塊,接下來對于每一個連通塊,找到邊權最小的出邊,然后合并兩個連通塊,

不斷重復這個操作,直到整張圖變成一個連通塊,

由于每次操作連通塊數量至少減半,所以時間復雜度最壞為 O((n + m) log n),隨機圖的話復雜度可以降到 O(n + m),

Tarjan演算法

Tarjan 演算法不是某個特定的演算法,而是一群演算法,

強連通分量

割點/割邊/橋

點雙連通分量

邊雙連通分量

離線 O(n) 求 LCA

此外還有很多 Tarjan 獨立/合作創造的演算法:

Splay,LCT, 斐波那契堆,斜堆,配對堆,可持久化資料結構,……

有向圖——強連通分量

如果對于兩個點 u, v,同時存在從 u 到 v 的一條路徑和從 v 到 u 的一條路徑,那么就稱這兩個點強連通,

如果一張圖的任意兩個點均強連通,那么就稱這張圖為強連通圖,

強連通分量指的是一張有向圖的極大強連通子圖, (極大≠最大)

Tarjan 演算法可以用來找出一張有向圖的所有強連通分量,

我們用 dfs的方式來找出一張圖的強連通分量,

建出 dfs 樹,記錄一下每一個節點的時間戳(dfn),然后我們考慮強連通分量應該滿足什么條件,

我們可以再記錄一個 low 陣列,表示每一個點能夠到達的最小的時間戳,如果一個點的 dfn=low,那么這個點下方就形成了一個強連通分量,

在 dfs 的程序中,對于 (u, v) 這條邊:

若 v 未被訪問,則遞回進去 dfs 并且用 low[v] 更新 low[u],

若 v 已經被訪問并且在堆疊中,則直接用 dfn[v] 更新 low[u],

最后如果 dfn[u]=low[u],則直接把堆疊中一直到 u 的所有點 拿出來作為一個強連通分量,

時間復雜度 O(n),

有向圖——縮點

跑出來強連通分量之后,我們可以把一個強連通分量看成一 個點,

接下來列舉所有的邊,如果是一個強連通分量里的就忽略, 否則連接兩個對應的強連通分量,這個操作稱為縮點,

縮點后就變成了一張有向無環圖,處理連通性問題的時候會方便很多,

無向圖——割點

對于一張無向圖,我們希望求出它的割點,

無向圖的割點定義為刪掉這個點之后,連通塊數量會發生改變的點,

類比上面,我們還是記錄一下 dfn(時間戳)和 low,

對于 u 的一個子節點 v,若 dfn[u]≤low[v],則 u 是割點(因 為 v 無法繞過 u 往上走),

不過需要注意兩點:

根節點不能用這種方法,而是應該看它的子節點數量是否大于等于 2,如果是那么根節點就是割點,

列舉出邊的時候要特判掉父子邊的情況,

無向圖——橋

無向圖的橋定義為刪掉這條邊后,連通塊數量會發生改變的邊,

和上面的方法幾乎一模一樣,唯一的區別是判斷dfn[u]<low[v]而不是dfn[u]≤low[v],(如果從 v 出發連 u 都無法 到達,那么 (u, v) 就是一個橋邊)

甚至連根節點都不需要特判了,

無向圖——點/邊雙連通分量

如果兩個點之間存在兩條點互不相交的路徑,那么就稱這兩個點是點雙連通的,

如果兩個點之間存在兩條邊互不相交的路徑,那么就稱這兩個點是邊雙連通的,

其余的定義參考強連通分量,

割點將整張圖分成了若干個點雙連通分量,并且一個割點可以在多個點雙連通分量中,

而橋則把整張圖拆成了若干個邊雙連通分量,并且橋不在任意一個邊雙連通分量中,

魔改一下強連通分量演算法即可,

當然,無向圖也可以縮點,不過主要還是可以用來建圓方樹,

二分圖匹配

前置知識

匹配

在圖論中,一個匹配是一個邊的集合, 其中任意兩條邊都沒有公共頂點,

最大匹配

一個圖所有匹配中,所含匹配邊數最多的匹配, 稱為這個圖的最大匹配,

如果要求一般圖的最大匹配,需要用 O(n 3 ) 的帶花樹,至少 是 NOI+ 的演算法,在聯賽階段,我們一般只關注二分圖的匹配問題,

(最大匹配——匈牙利演算法)

完美匹配

如果一個圖的某個匹配中,所有的頂點都是匹配點,那么它就是一個完美匹配,

二分圖

如果一個圖的頂點能夠被分為兩個集合 X, Y,滿 足每一個集合內部都沒有邊相連,那么這張圖被稱作是一張二分圖,

(dfs可以判斷一張圖是否是二分圖)

交替路

從一個未匹配點出發,依次經過非匹配邊——匹配 邊——非匹配邊——……形成的路徑叫交替路,

增廣路

從一個未匹配點出發,依次經過非匹配邊——匹配 邊——非匹配邊——……——非匹配邊,最后到達一個未匹配點形成的路徑叫增廣路,

注意到,一旦我們找出了一條增廣路,將這條路徑上所有匹配邊和非匹配邊取反,就可以讓匹配數量+1,

匈牙利演算法就是基于這個原理,

假設我們已經得到了一個匹配,希望找到一個更大的匹配,

我們從一個未匹配點出發進行 dfs(深度優先搜索),如果找出了一個增廣路, 就代表增廣成功,我們找到了一個更大的匹配,

如果增廣失敗,可以證明此時就是最大匹配,

由于每個點只會被增廣一次,所以時間復雜度是 O(n(n + m)),

二分圖最大權匹配——KM 演算法

現在我們把所有的邊都帶上權值,希望求出所有最大匹配中權值之和最大的匹配,

我們的思路是給每一個點賦一個“期望值”,也叫作頂標函式 c,對于 (u, v) 這條邊來說,只有 c(u) + c(v) = w(u, v) 的時 候,才能被使用,

容易發現,此時的答案就是 ∑c(i),

初始,我們令左邊所有點的 c(u) = maxv w(u, v),也就是說最理想的情況下,每一個點都被權值最大的出邊匹配,

接下來開始增廣,每次只找符合要求的邊,我們定義只走這些邊訪問到的子圖為相等子圖,

如果能夠找到增廣路就直接增廣,否則,就把這次增廣訪問到的左邊的所有點的 c ? 1,右邊所有點的 c + 1,

經過這樣一通操作,我們發現原來的匹配每一條邊仍然滿足條件,同時由于訪問到的點左邊比右邊多一個(其余的都匹配上了),所以這樣會導致總的權值?1,

接下來再嘗試進行增廣,重復上述程序,直接這樣做時間復 雜度是 O(n 3 c) 的,(進行 n 次增廣,每次修改 c 次頂標,訪問所有 n 2 條邊)

優化

由于修改頂標的目標是讓相等子圖變大,因此可以每次加減 一個最小差值 delta,這樣每次增廣只會被修改最多 n 次頂標,時間復雜度降到 O(n 4 ),

注意到每次重新進行 dfs(深度優先搜索) 太不優秀了,可以直接進行 bfs, 每次修改完頂標之后接著上一次做,時間復雜度降到 O(n 3 ),

技巧

最小點覆寫

選取最少的點,使得每一條邊的兩端至少有一 個點被選中,

二分圖的最小點覆寫 = 最大匹配

證明

1.由于最大匹配中的邊必須被覆寫,因此匹配中的每一個點對 中都至少有一個被選中,

2.選中這些點后,如果還有邊沒有被覆寫,則找到一條增廣路,矛盾,

最大獨立集:選取最多的點,使得任意兩個點不相鄰,

最大獨立集 = 點數-最小點覆寫

證明

1.由于最小點覆寫覆寫了所有邊,因此選取剩余的點一定是一個合法的獨立集,

2.若存在更大的獨立集,則取補集后得到了一個更小的點覆寫,矛盾,

最小邊覆寫:選取最少的邊,使得每一個點都被覆寫,

最小邊覆寫 = 點數-最大匹配

證明

1.先選取所有的匹配邊,然后對剩下的每一個點都選擇一條和 它相連的邊,可以得到一個邊覆寫,

2.若存在更小的邊覆寫,則因為連通塊數量 = 點數-邊數,這 個邊覆寫在原圖上形成了更多的連通塊,每一個連通塊內選一條邊,我們就得到了一個更大的匹配,

最小不相交路徑覆寫:一張有向圖,用最少的鏈覆寫所有的點,鏈之間不能有公共點,

將點和邊分別作為二分圖的兩邊,然后跑匹配,最小鏈覆寫 = 原圖點數-最大匹配,

最小可相交路徑覆寫:一張有向圖,用最少的鏈覆寫所有的 點,鏈之間可以有公共點,

先跑一遍傳遞閉包,然后變成最小不相交路徑覆寫,

補充

小黃鴨除錯法

當你的代碼出現問題的時候,

將小黃鴨想象成你的同學,

將你的代碼一行一行地講給它,

也許講到一半你就知道問題出在哪了,

不要定義以下變數名

next,abs,x1,y1,size……

并非原創,僅是整理,請見諒

Lo問我為什么看星星,我覺得銀河和代碼是同一種東西,這也是一種回答,————Co

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/451995.html

標籤:其他

上一篇:java實作稀疏矩陣的壓縮與解壓

下一篇:10行代碼實作一個值班提醒應用

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more