CSP複賽提高組知識點(1)-圖論(上)

**

圖論是歷年信息學複賽考點之一。今天我們來帶大家複習一下圖論相關知識!

**
圖論

一、圖的存儲:

1、鄰接矩陣:
假設有n個節點,建立一個n×n的矩陣,第i號節點能到達第j號節點就將[i][j]標記爲1(有權值標記爲權值),
樣例如下圖:

/無向圖,無權值/
int a[MAXN][MAXN];//鄰接矩陣
int x,y;//兩座城市
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
scanf("%d%d",&x,&y);//能到達,互相標記爲1
a[x][y]=1;
a[y][x]=1;
}
}
/無向圖,有權值/
int a[MAXN][MAXN];//鄰接矩陣
int x,y,w;//兩座城市,路徑長度
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
scanf("%d%d%d",&x,&y,&w);//能到達,互相標記爲權值w
a[x][y]=w;
a[y][x]=w;
}
}
/有向圖,無權值/
int a[MAXN][MAXN];//鄰接矩陣
int x,y;//兩座城市
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
scanf("%d%d",&x,&y);//能到達,僅僅是x到y標記爲1
a[x][y]=1;
}
}
/有向圖,有權值/
int a[MAXN][MAXN];//鄰接矩陣
int x,y,w;//兩座城市,路徑長度
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
scanf("%d%d%d",&x,&y,&w);//能到達,僅僅是x到y標記爲權值w
a[x][y]=w;
}
}

鄰接矩陣很方便,但是在n過大或者爲稀疏圖時,就會很損耗時空,不建議使用!

2.鄰接表:
鄰接表是一個二維容器,第一維描述某個點,第二維描述這個點所對應的邊集們。
鄰接表由表頭point,鏈點構成,如下圖是一個簡單無向圖構成的鄰接表:

我們可以用指針來創建鏈表,當然,這是很複雜也很麻煩的事情,下面來介紹一種用數組模擬鏈表的方法:
//有向圖鄰接表存儲
const int N=1005;
const int M=10050;
int point[N]={0};//i節點所對應鏈表起始位置(表頭)
int to[M]={0};
int next[M]={0};//i節點下一個所指的節點
int cc=0;//計數器(表示第幾條邊)
void AddEdge(int x,int y)//節點x到y
{
cc++;
to[cc]=y;
next[cc]=point[x];
point[x]=cc;
}
void find(int x)
{
int now=point[x];
while(now)
{
printf("%d\n",to[now]);
now=next[now];
}
}
int main()
{

}

具體的過程我也不是很懂怎麼描述,反正如果要加強記憶的話可以用我所給的例子模擬一下point[],to[],next[],然後再調用函數find(x)來輸出x這個節點能到的點,大概就能YY到數組是怎麼存儲鄰接表的了。
還是不理解的話,推一個blog,這裏面說的和我這裏給出的思路很相似:http://developer.51cto.com/art/201404/435072.htm

二、樹的遍歷:

1.BFS:運用隊列,一開始隊列中有一個點, 將一個點出隊,將它的子結點全都入隊。
算法會在遍歷完一棵樹中每一層的每個結點之後,纔會轉到下一層繼續,在這一基礎上,隊列將會對算法起到很大的幫助:

//廣度優先搜索
void BreadthFirstSearch(BitNode root)
{
queue<BitNode
> nodeQueue;
nodeQueue.push(root);//將根節點壓入隊列
while (!nodeQueue.empty())//隊列不爲空,繼續壓入隊列
{
BitNode *node = nodeQueue.front();
nodeQueue.pop();//彈出根節點
if (node->left)//左兒子不爲空
{
nodeQueue.push(node->left);//壓入隊列
}
if (node->right)//右兒子不爲空
{
nodeQueue.push(node->right);//壓入隊列
}
}
}

2.DFS:運用棧,遞歸到一個點時,依次遞歸它的子結點。
還可以利用堆棧的先進後出的特點,現將右子樹壓棧,再將左子樹壓棧,這樣左子樹就位於棧頂,可以保證結點的左子樹先與右子樹被遍歷:

//深度優先搜索
//利用棧,現將右子樹壓棧再將左子樹壓棧
void DepthFirstSearch(BitNode root)
{
stack<BitNode
> nodeStack;
nodeStack.push(root);//將根節點壓棧
while (!nodeStack.empty())//棧不爲空,繼續壓棧
{
BitNode *node = nodeStack.top();//引用棧頂
cout << node->data << ’ ';
nodeStack.pop();//彈出根節點
if (node->right)//優先遍歷右子樹
{
nodeStack.push(node->right);
}
if (node->left)
{
nodeStack.push(node->left);
}
}
}

三、無根樹變成有根樹:

選擇一個點作爲根結點, 開始遍歷。
  遍歷到一個點時, 枚舉每一條連接它和另一個點的邊。若另一個點不是它的父結點, 那就是它的子結點。遞歸到子結點。
  我們可以更加形象的比喻爲:抓住一個點,把它拎起來構成一棵新的樹。

四、並查集:

這是我學OI這麼久以來覺得性價比最高的算法(簡單又實用啊!!),用來處理不相交合並和查詢問題。
給大家推個超超超超級易懂的blog,保證一看就懂,這裏我就不再詳解了:http://blog.csdn.net/dellaserss/article/details/7724401

五、最小生成樹:

1.Prim算法(適用於稠密圖):
算法描述:
1).輸入:一個加權連通圖,其中頂點集合爲V,邊集合爲E;
2).初始化:Vnew = {x},其中x爲集合V中的任一節點(起始點),Enew = {},爲空;
3).重複下列操作,直到Vnew = V:
a.在集合E中選取權值最小的邊<u, v>,其中u爲集合Vnew中的元素,而v不在Vnew集合當中,並且v∈V(如果存在有多條滿足前述條件即具有相同權值的邊,則可任意選取其中之一);
b.將v加入集合Vnew中,將<u, v>邊加入集合Enew中;
4).輸出:使用集合Vnew和Enew來描述所得到的最小生成樹。

#include<stdio.h>//普里姆算法
const int N=1050;
const int M=10050;
struct Edge//定義圖類型結構體,a到b權值爲c
{
int a,b,c;
}edge[M];
int n,m;//n個點,m條邊
bool black[N];//染黑這個點,表示這個點已經被選過了
int ans=0;//最小生成樹權值和
int main()
{
int i,j,k;
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
scanf("%d%d%d",&edge[i].a,&edge[i].b,&edge[i].c);
black[1]=1;//把第一個點染黑(從第一個點找起)
for(k=1;k<n;k++)
{
int mind,minz=123456789;
for(i=1;i<=m;i++)//開始!
{
if(black[edge[i].a]!=black[edge[i].b]&&edge[i].c<minz)//如果這個點未被找過並且權值比當前最優值還要小,更新之
{
mind=i;//記錄當前最優點
minz=edge[i].c;//記錄當前最小邊權
}
}
////將這最優點歸入
ans+=minz;//答案加上
black[edge[mind].a]=1;//染黑兩個節點
black[edge[mind].b]=1;
/
/
}
printf("%d\n",ans);//輸出答案
return 0;
}

2.kruskal算法(適用於稀疏圖):

算法描述:
克魯斯卡爾算法從另一途徑求網的最小生成樹。
假設連通網N=(V,{E}),則令最小生成樹的初始狀態爲只有n個頂點而無邊的非連通圖T=(V,{∮}),圖中每個頂點自成一個連通分量。
在E中選擇代價最小的邊,若該邊依附的頂點落在T中不同的連通分量上,則將此邊加入到T中,否則捨去此邊而選擇下一條代價最小的邊。
依次類推,直至T中所有頂點都在同一連通分量上爲止。

#include//克魯斯卡爾算法
#include
#include
using namespace std;
const int N=1050;
const int M=10050;
struct Edge//定義圖類型結構體
{
int a,b,c;//a到b的權值爲c
}edge[M];
int fa[N];//父親數組
int n,m;//n個節點,m條邊
int ans=0;//最小生成樹權值和
bool cmp(Edge x,Edge y)//比較權值大小
{
return (x.c<y.c);
}
int getf(int x)//尋找x的最原始祖先(並查集)
{
if(fa[x]!=x)
fa[x]=getf(fa[x]);
return fa[x];//返回最原始祖先
}
int main()
{
int i,j;
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
scanf("%d%d%d",&edge[i].a,&edge[i].b,&edge[i].c);
sort(edge+1,edge+m+1,cmp);//從小到大排序邊數組
for(i=1;i<=n;i++)
fa[i]=i;//初始值,每個節點的父親就是自己
for(i=1;i<=m;i++)
{
int a=edge[i].a;
int b=edge[i].b;
a=getf(a);//尋找a的最原始祖先
b=getf(b);//尋找b的最原始祖先
if(a!=b)//如果兩個的最終祖先不相同(不會構成迴路)
{
ans+=edge[i].c;//加入
fa[a]=b;//加入當前父親的兒子們中(合併並查集)
}
}
printf("%d\n",ans);
return 0;
}

經典例題:繁忙的都市(Luogu 2330)
城市C是一個非常繁忙的大都市,城市中的道路十分的擁擠,於是市長決定對其中的道路進行改造。城市C的道路是這樣分佈的:城市中有n個交叉路口,有些交叉路口之間有道路相連,兩個交叉路口之間最多有一條道路相連接。這些道路是雙向的,且把所有的交叉路口直接或間接的連接起來了。每條道路都有一個分值,分值越小表示這個道路越繁忙,越需要進行改造。但是市政府的資金有限,市長希望進行改造的道路越少越好,於是他提出下面的要求:
1.改造的那些道路能夠把所有的交叉路口直接或間接的連通起來。
2.在滿足要求1的情況下,改造的道路儘量少。
3.在滿足要求1、2的情況下,改造的那些道路中分值最大的道路分值儘量小。
任務:作爲市規劃局的你,應當作出最佳的決策,選擇那些道路應當被修建。
這題是經典的最小瓶頸生成樹問題:只用邊權小於等於x的邊,看看能不能構成最小生成樹。
在kruskal算法中,我們已經對邊從小到大排過序了,所以只要用≤x的前若干條邊即可。

3.最小生成樹計數問題:
題目:現在給出了一個簡單無向加權圖。你不滿足於求出這個圖的最小生成樹,而希望知道這個圖中有多少個不同的最小生成樹。(如果兩顆最小生成樹中至少有一條邊不同,則這兩個最小生成樹就是不同的)。
解法:按邊權排序,先選小的,相同邊權的暴力求出有幾種方案,將邊按照權值大小排序,將權值相同的邊分到一組,統計下每組分別用了多少條邊。然後對於每一組進行dfs,判斷是否能夠用這一組中的其他邊達到相同的效果。最後把每一組的方案數相乘就是答案。
換句話說:就是不同的最小生成樹方案,每種權值的邊的數量是確定的,每種權值的邊的作用是確定的, 排序以後先做一遍最小生成樹,得出每種權值的邊使用的數量x然後對於每一種權值的邊搜索,得出每一種權值的邊選擇方案。
#include
#include
#define N 105
#define M 1005
#define MOD 31011
using namespace std;
struct node//定義圖類型結構體
{
int a,b;//節點a,b
int zhi;//a到b的權值
}xu[M];
int n,m;
int fa[N];
int lian[N];
int ans=1;
int cmp(struct node x,struct node y)//從小到大排序函數
{
return (x.zhi<y.zhi);
}
int getf(int x)
{
if(fa[x]!=x)
fa[x]=getf(fa[x]);
return(fa[x]);
}
int getlian(int x)
{
if(lian[x]x)
return x;
return ( getlian(lian[x]) );
}
int dfs(int now,int end,int last)
{
if(now
end)
{
if(last0)
return 1;
return 0;
}
int res=dfs(now+1,end,last);
int s=getlian(xu[now].a);
int t=getlian(xu[now].b);
if(s!=t)
{
lian[s]=t;
res+=dfs(now+1,end,last-1);
lian[s]=s;
}
return res;
}
int main()
{
int i,j,k;
int s,t;
int now;
int sum=0;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++)//初始化,每個節點的父親就是自己
fa[i]=i;
for(i=1;i<=m;i++)
scanf("%d%d%d",&xu[i].a,&xu[i].b,&xu[i].zhi);
sort(xu+1,xu+m+1,cmp);//從小到大排序邊數組
for(i=1;i<=m;)
{
for(j=1;j<=n;j++)
lian[j]=j;
k=i;
while(i<=m&&xu[i].zhi
xu[k].zhi)
{
xu[i].a=getf(xu[i].a);
xu[i].b=getf(xu[i].b);
i++;
}
now=sum;
for(j=k;j<i;j++)
{
s=getf(xu[j].a);
t=getf(xu[j].b);
if(s!=t)
{
sum++;
fa[s]=t;
}
}
ans*=dfs(k,i,sum-now);
ans%=MOD;//防止溢出
}
if(sum!=n-1)
ans=0;
printf("%d\n",ans);
return 0;
}

發佈了18 篇原創文章 · 獲贊 5 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章