圖論算法(筆記)

存圖:

鏈向星優化

//no weight graph
vector<int> edge[100];
int main(){
    for(int i = 1; i <= n; i++){
        int uu, vv;
        cin >> uu >> vv;
        edge[uu].push_back(vv);
        //no direction
        edge[vv].push_back(uu);
    }
}
//weight graph
//first is vertex, second is weight
vector<pair<int, int> > edge[100];
int main(){
    for(int i = 1; i <= n; i++){
        int u, v, w;
        cin >> u >> v >> w;
        edge[u].push_back({v, w});
    }
}

Floyd 算法

找點與點之間的最短路徑

思想簡單 就是固定某個點中介點, 然後求該點與其他所有點的最短距離求出來, 那就可以把需要經過這個中介點的最短距離求出來, 依次把所有最短路徑找出.

時間複雜度: O(V^3) V為點的數目

int n, m;
vector<vector<int, int>> aj_matrix(1000, vector<int>(1000, INT_MAX));
void floyd(){
    for(int k = 1; k <= n; k++){
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= n; j++){
                aj_matrix[i][j] = min(aj_matrix[i][k] + aj_matrix[k][j], aj_matrix[i][j]);
            }
        }
    }
}
int main(){
    cin >> n >> m;
    for(int i = 1; i <= m; i++){
        int u, v, dis;
        cin >> u >> v >> dis;
        aj_matrix[u][v] = dis;
    }
    floyd();
}

Floyd算法 雖然本質不難, 但在某些情況 能夠產生變化, 比如P2419這一題. 

需要確認點與點之間的關係, 由於存在某些點可以通過中介點到達其他點, 所以比較難做, 但是如果我們使用floyd算法, 把最短距離變為 布林值的 可否到達, 我們就能知道所有點之間的關係了.

從是否和所有點有關係的這一個角度, 判斷是否有排名.

Dijkstra 算法

Floyd 算法的複雜度偏高, 把所有點的最短路都求出來了, 如果我們只要求求單源最短路, 如何優化?

這就是Dijkstra算法的優勢了. 求某一個點到所有點的最短距離.

利用一個dis 數組, 在bfs的時候, 不斷把每個點的最優路徑更新.

中間利用了貪心的思想, 先把local 最優的點先遍歷.

struct node{
	int v, w;
	node(int v = 0, int w = 0):v(v), w(w){};
	bool operator < (const node &a)const{return a.w < w;}
};
vector<node> edge[200005];
vector<int> dis(200005, 1e9);
vector<bool> vis(200005, 0);
void dijkstra(){
	priority_queue<node> q;
	dis[s] = 0;
	q.push(node(s));
	while(!q.empty()){
		int u = q.top().v;
		q.pop();
		if(vis[u])continue;
		vis[u] = 1;
		for(auto a : edge[u]){
			if(dis[u] + a.w < dis[a.v]){
				dis[a.v] = dis[u] + a.w;
				q.push(node(a.v, dis[a.v]));
			}
		}
	}
}

列題 P4568 飛行路線

這道題如果沒有免費機票會非常好做, 因為直接利用DIjistra直接找最短路徑就可以了.

我們可以利用分層圖這個思想來解決問題.

我們可以透過複製原圖k遍.

然後把每層的點連接起來, 層與層之間的連接花費為0元, 就可以解決免費機票的問題.

如何建立不同層呢?

假設原圖只有n個vertex, 那麼下一層的起始節點 就是n, 下下層就是 n*2, 如此類推.

最後我們返回 k*n + 終點. 就可以了. 

為了讓自己記住分層圖的原理, 畫一個 不是那麼完美的圖 

當然如果我們在中間層的時候就到了終點, 由於我們只會輸出 n * k + end 這個節點的成本, 所以為了讓這種情況不發生, 我們讓每一層的終點都能免費到達 n *k + end這個節點.

這種概念會經常用到, 所以銘記於心吧.

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<set>
#include<queue>
#include<map>
using namespace std;
typedef long long ll;
int n, m, k, s, t;
struct node{
	int v, w;
	node(int v = 0, int w = 0):v(v), w(w){};
	bool operator < (const node &a)const{return a.w < w;}
};
vector<node> edge[200005];
vector<int> dis(200005, 1e9);
vector<bool> vis(200005, 0);
void dijkstra(){
	priority_queue<node> q;
	dis[s] = 0;
	q.push(node(s));
	while(!q.empty()){
		int u = q.top().v;
		q.pop();
		if(vis[u])continue;
		vis[u] = 1;
		for(auto a : edge[u]){
			if(dis[u] + a.w < dis[a.v]){
				dis[a.v] = dis[u] + a.w;
				q.push(node(a.v, dis[a.v]));
			}
		}
	}
}
void work(){
	cin >> n >> m >> k >> s >> t;
	for(int i = 1; i <= m; i++){
		int u, v, w;
		cin >> u >> v >> w;
		edge[u].push_back(node(v, w));
		edge[v].push_back(node(u, w));
		for(int j = 1; j <= k; j++){
			//level i connect to level i + 1 next with weight 0
			edge[u + (j - 1) * n].push_back(node(v + j * n));
			edge[v + (j - 1) * n].push_back(node(u + j * n));
			//level i + 1 connect to level i + 1 next with weight, like white black cross 
			edge[u + j * n].push_back(node(v + j * n, w));
			edge[v + j * n].push_back(node(u + j * n, w));
		}
	} 
	//if we reach to t, just give a free weight path to t + k*n
	for(int i = 1; i <= k; i++){
		edge[t + (i - 1)*n].push_back(node(t + i * n));
	}
	dijkstra();
	cout << dis[t + k*n];
}
int main(){
	ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
	work();
	return 0;
}

 SPFA 算法

由於邊權為負數的時候, 我們無法使用dijkstra算法 (優先隊列會無法將最優值放堆頂), 而這個時候spfa 算法就很有用了.

這個算法也不難. 

只要發現到達某個點的成本更新了, 就把這個點放進隊列, 繼續更新後繼節點的成本, 循環這個操作, 所以同一個點可以進入隊列很多次. 

與dijkstra算法不同, 我們需要有一個inq 數組, 記錄哪些點已經在隊列中了, 因為在裡面就沒必要再加進去造成不必要的時間消耗.

// here skip storing the edge, just the same code as above 
queue<pair<int, int> > q;
int inq[1000000];
vector<int> dis(10000, INT_MAX);
void SPFA(int s){
	memset(inq, 0, sizeof(inq));
	q.push({s, 0});
	dis[s] = 1;
	inq[s] = 1
	while(q.size()){
		int u = q.front().first;
		inq[u] = 0;
		for(auto a : edge[u]){
			int v = a.first;
			int w = a.second;
			if(dis[u] + w < dis[v]){
				dis[v] = dis[u] + w;
				if(!inq[v]){
					q.push({v, dis[v]});
					inq[v] = 1;
				}
			}
		}
	}
}

時間複雜度是 O(kE)

如果可以使用dijkstra 就不要用spfa

並查集

這裡介紹並查集方便一會 重溫樹.

並查集用於查詢兩個元素是否在同一個集合裡, 你也可以說是否在同一個連通塊.

它通常支持兩種操作, 第一種為合併操作, 第二種為查詢操作.

合併操作有什麼用呢? 

就是把兩個不同的集合合併起來.

-----------------------------------------------------------------------------------

所以以後你一需要知道某些元素是否在同一個集合中, 那你就放心往並查集的方向想把

-----------------------------------------------------------------------------------

如果你想形象化並查集, 你可以把他當成有根樹, 每個不同的集合都是一顆樹, 所以整個並查集是一個森林.

代碼也非常簡潔, 直接利用數組實現就可以了.

數組是記錄每個節點的上一個節點, 如果一個節點上面沒有節點會標為他自己.

查詢的代碼

int fa[1000];
int root(int x){
    if(x == fa[x])return x;
    return root(fa[x]);
}
int init(){
    for(int i = 1; i <= n; i++){
        fa[i] = i;
    }
}

合併的代碼

合併也非常簡單, 直接把兩個最根節點 拿其中一個做另一個的父親.

int union(int x, int y){
    int fx = find(x);
    int fy = find(y);
    if(fx != fy){
        p[fx] = fy;
    }
}

但其實對於查詢操作, 我們是可以進行優化的!!!

為什麼呢, 看看以下這個圖片

 這就是優化的思路.

代碼實現超簡單, 只需要在查詢的時候同時更新每個點就可以了.

int find(int x){
    if(fa[x] == x)return x; 
    return fa[x] = find(x);
}

那麼合併是否可以優化呢?

可以, 我們讓深度較低的樹指向深度較高的樹就可以了.

帶權並查集 <------- 有待更新

什麼是最小生成樹?

-對於一個無向連通圖, 生成樹就是他的子圖, 符合以下要求, 它是包含所有vertex的樹.

-最小生成樹就是他的邊權合是最小的

那麼如何查找最小生成樹呢?

那就要引入kruskal 算法

Kruskal 算法

Kruskal 算法需要用到並查集進行維護.

Kruskal算法的核心思想就是貪心.

我們首先把所有邊進行排序, 然後依次抽出較小的邊出來, 把他放進我們的答案中.

為了不讓環出現, 我們利用並查集查詢和維護構造生成樹的正確性.

由於我們只對邊進行了排序和抽取, 時間複雜度是 O(Elog(E)).

//Kruskal
//unionfind array
int fa[100000];
struct node{
	int u, v, w;
	bool operator <(const node & a)const{
		return a.w < w;
	}
}edge[100000];
int find(int x){
	if(fa[x] == x)return fa[x] = find(x);
}
int kruskal(){
	int cnt = 0;
	int ans = 0;
	sort(edge + 1, edge + m + 1);
	for(int i = 1; i <= m; i++){
		if(cnt == n - 1)break;
		int u = edge[i].u;
		int v = edge[i].v;
		int w = edge[i].w;
		int fu = find(u);
		int fv = find(v);
		if(fu == fv)continue;
		else{
			fa[fu] = fv;
			cnt++;
			ans+= w;
		}
	}
	return ans;
}
int work(){
	//init unionfind array
	for(int i = 1; i <= n; i++){
		fa[i] = i;
	}
	for(int i = 1; i <= m; i++){
		cin >> edge[i].u >> edge[i].v >> edge[i].w;
	}
	kruskal();
}

你會發現這個圖的存儲跟我前面的不一樣, 因為kruskal需要排序完還能提取兩個端點的資料, 這樣直接把3個信息放在一個struct裡會方便超級多!

列題:

p1195

仔細想想所有點都能到達所有點是什麼意思就好了.

P1194 

其實就是建立超級點, 作為中介點, 排除所有比a 高的權值. 超級點能連像任何人.

看到上面幾題, 你會發現, 算法幾乎都是套模板, 變得只是存圖技巧.

拓撲排序

DAG 有向無環圖.

所有DAG 存在一個或對多個拓撲排序.

其實就是從入度為0的vertex開始遍歷, 利用bfs 把全部點遍歷出來.

時間複雜度為 O(V + E)

需要用一個入度數組, 把下一層的點的入度進行維護, 每遍歷了一個指向該點的點, 就把他的入度減一, 我們只會在入度為0的時候, 把點加進隊列中

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m,head[N],tot=0,ind[N],a[N],cnt;
vector<int>G[N];
inline void addedge(int u,int v){
	G[u].push_back(v);
}
inline int read(){
	int f=1,x=0;char ch;
	do{ch=getchar();if(ch=='-')f=-1;}while(ch<'0'||ch>'9');
	do{x=x*10+ch-'0';ch=getchar();}while(ch>='0'&&ch<='9');
	return f*x;
}
queue<int> q;
inline void toposort(){
	for(int i=1;i<=n;i++)if(ind[i]==0)q.push(i);
	while(!q.empty()){
		int u=q.front();q.pop();
		a[++cnt]=u;
		for(int i=0;i<G[u].size();i++){
			int v=G[u][i];
			ind[v]--;
			if(ind[v]==0)q.push(v);
		} 
	}
}
int main(){
	n=read();m=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read();
		addedge(u,v);
		ind[v]++;
	}
	toposort();
	for(int i=1;i<=cnt;i++)printf("%d%c",a[i],i==cnt?'\n':' ');
}

待完成題目 

P1462

P1993

P2700

P1186

P1807

P1038

P4001

P2330

P4047

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值