DFS的结果就是一颗
搜索树
,只不过每次只记录眼前的分支,然后通过栈回溯到上一个节点再往下朝另一个方向搜索,绘出所有轨迹就是一棵搜索树。
#include
using namespace std;
const int N=8;
int n,path[N];
bool st[N];
void dfs(int u){
if(u==n){
for(int i=0;i<n;i++) cout<<path[i]<<' ';
cout<<endl;
return ;
}
for(int i=1;i<=n;i++){
if(!st[i]){
path[u]=i;
st[i]=true;
dfs(u+1);
st[i]=false;
}
}
}
int main()
{
cin>>n;
dfs(0);
return 0;
}
#include
#include
#include
using namespace std;
const int N=15;
int n;
vector<vector<char>> g(N,vector<char>(N,'.'));
bool row[N],col[N],bias_l[N],bias_r[N];
void dfs(int layer){
if(layer==0){
for(int x=n;x>0;x--){
for(int y=1;y<=n;y++){
cout<<g[x][y];
}
cout<<endl;
}
cout<<endl;
return;
}
for(int y=1;y<=n;y++){
if(!row[layer]&&!col[y]&&!bias_r[layer+y]&&!bias_l[y-layer+n]){
g[layer][y]='Q';
row[layer]=col[y]=bias_r[layer+y]=bias_l[y-layer+n]=true;
dfs(layer-1);
g[layer][y]='.';
row[layer]=col[y]=bias_r[layer+y]=bias_l[y-layer+n]=false;
}
}
}
int main()
{
cin>>n;
dfs(n);
return 0;
}
不难发现,这两道题目完全不一样,但题解的结构几乎是一致的。
而剪枝
是算法效率高低的核心所在,也是难点所在。
BFS的搜索过程就是一个雷达扫描图,一层一层向外搜索,基于这个特点,如果边权相同可以用来解最短路问题。
#include
#include
using namespace std;
const int N=110;
typedef pair<int,int> PII;
vector<vector<int> > d(N,vector<int>(N,-1));//和g[N][N]一样大的二维数组,存储当前节点到原点的距离,-1表示未被访问
int n,m,hh,tt;//队列头尾指针
PII q[N*N];//模拟队列(因为是模拟所以出队后头指针会向后走,因此队列长度不能小于可能入队元素个数)
int g[N][N];//矩阵信息
int bfs(){
q[0]={0,0};//将左上角节点入队
d[0][0]=0;//左上角到自己的距离为0
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};//枚举四个方向本质还是坐标的偏移
while(hh<=tt){
auto t=q[hh++];//要擅用auto
for(int i=0;i<4;i++){//枚举四个方向
int x=t.first+dx[i],y=t.second+dy[i];
if(x>=0&&x<n&&y>=0&&y<m&&d[x][y]==-1&&g[x][y]==0){//d[N][N]既存储距离又充当判断是否走过
q[++tt]={x,y};
d[x][y]=d[t.first][t.second]+1;
}
}
}
return d[n-1][m-1];
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
cin>>g[i][j];
cout<<bfs();
return 0;
}
DFS一般没有固定的思路,但BFS往往有一套组合拳。
通过int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
可以使用一个for循环相当简洁的遍历4邻域,8邻域同理。本是上是构建了一个向量。
以后会在图论中大量见到d[N][N]
这个数组,妙用无穷
,例如在本题,既可以表示某个坐标是否走过
,还可以知道距离起点多远
。甚至还可以记录当前点是从哪一个父节点转移过来的,这样可以从终点回溯出一条路径
。
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
来进行一个遍历,使用个map来存储这个串是否出现过(也就是某个点是否走过,key=string,value=distance)。基于这个题目是尝试先走,也就是在遍历每个方向后都需要回溯状态,方便本次循环中下一次尝试。
树是有向无环图
,在存储时无向图
又是在有向图
的基础上添加一条逆向的边罢了。因此我们在存储时只需要会存有向图即可。
g[N][N]
,g[a][b]
表示有一条从a->b
的边。适合稠密图h[N]
中,然后每个h[a]
采用链表的方式,b遍历h[a]
这个链表能找到b
,说明有一条从a->b
的边。适合稀疏图,这个用得较多。// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 添加一条边a->b
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++ ;
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);
注意:e存的是边数,往往与N也就是节点的个数不一致。
深度优先搜索
和广度优先搜索
一般来收,深搜都会涉及到递归,利用系统的栈,具体代码因题而异。而宽搜一般少不了队列。并且一般都少不了一个
st[N]
标记某个点是否被访问过了或者d[N][N]
表示某个点到起点的距离,深搜常手写邻接表,但广搜常直接用STL的queue
。基于这个思路代码会好些不少。
int dfs(int u)
{
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);
while (q.size())
{
int t = q.front();
q.pop();
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true; // 表示点j已经被遍历过
q.push(j);
//这里可以加上距离
}
}
}
只针对有向图,无向图是没有拓扑序的。简而言之,将图的所有顶点按某个序列排好,这个序列中任意两个有边的顶点都是从前指向后的。而没有从后指向前的点是不是就可以推出这是一个无环图~也就是说拓扑序可以判断图中是否存在环。
入度
、出度
。入度为0意味着没有其他边指向它,从它开始处理邻接点就不会存在存在某条边从后指向前的问题了,所以我们选取入度为0的点作为起点。一般步骤:
#include
#include
using namespace std;
int n,m;
const int N=1e5+10;
vector<int> h(N,-1);//邻接表的静态部分(这个数组范围也是1~n)
int idx,e[N],ne[N];//邻接表的链表部分
int hh=0,tt=-1,q[N];//数组模拟队列,tt设为-1是因为待会要循环先执行入队操作,保持编号与下标一致
int d[N];//记录每个点的入度
bool topsort(){
for(int i=1;i<=n;i++){
if(!d[i]) q[++tt]=i;
}
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
d[j]--;
if(!d[j]) q[++tt]=j;
}
}
if(tt==n-1) return true;
else return false;
}
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++){
int a,b;
cin>>a>>b;
add(a,b);
d[b]++;
}
if(topsort()){
for(int i=0;i<n;i++) cout<<q[i]<<' ';
}else cout<<-1;
return 0;
}
时间复杂度O(n^2),与源点个数n有关,与边的数目m无关,适合稠密图。
const int N=510,INF=0x3f3f3f3f;
vector> g(N,vector(N, INF));//邻接矩阵
vector dist(N,INF);//到起点的最短距离
bool st[N];//节点是否纳入最短点集
#include
#include
#include
using namespace std;
const int N=510,INF=0x3f3f3f3f;
vector<vector<int>> g(N,vector<int>(N, INF));
vector<int> dist(N,INF);
bool st[N];
int n,m;
int dijkstra(){
dist[1]=0;
for(int i=0;i<n;i++){//处理n次,每次选取一个点纳入最短点集,n次后所有点都纳入最短点集合了,因此任意一个点到起点的距离都是最短的
int t=-1;
for(int j=1;j<=n;j++){
if(!st[j] && (t==-1 || dist[t]>dist[j])){
t=j;
}
}
st[t]=true;//这里意味着每次循环都有一个点纳入最短点集,但距离的更新就不好说了,没有边可能还是无穷
for(int j=1;j<=n;j++){
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
}
if(dist[n]==INF) return -1;
else return dist[n];
}
int main()
{
cin>>n>>m;
while(m--){
int x,y,z;
cin>>x>>y>>z;
g[x][y]=min(g[x][y],z);
}
cout<<dijkstra();
return 0;
}
(假设求第1个点到第n个点的最短路)
1.所有点到起点的距离都是无穷,此时最短点集为空。
2.起点到自己的距离为0,是所有距离起点最近的点。注意:此时还未将起点纳入最短点集,只是为其赋予了关键的初始值
1.在所有还未纳入最短点集的点中找一个距离起点最近的点,2.将这个点纳入最短点集
3.然后利用这个点更新其他点的距离。
将上面朴素Dijkstra算法的步骤表达出来,发现
时间复杂度最高的就是在不属于最短点集的点中找出dist最小的
,最坏形况下遍历n个点,时间复杂度O(n^2)。如果我们把所有的dist插入小根堆中,那么每次找到的时间就是近乎O(1),而第三步用t更新其他点的距离本质上是在遍历边m
,也就是n次更新一共遍历了m
条边,每次维护队中的n个节点堆插入操作的时间复杂的就是logn,最大的时间复杂的就为m*logn
。也就是说适合稀疏图m,n一个数量级的。但是STL中的堆是不支持指定删除节点的,也就是说我们更新后的更短的dist只能直接插入,但是由于更新后值更小,所以并不会受到这个点之前未删除的值的影响,形成空间换时间。并且由于冗余的存在,可能会出现某个点已经被纳入最短点集了,但是某一次堆中pop出的是那个被纳入最短点集的点上一次的较小值,但这个较小值可能比堆中其他所有点的dist都要小,所以我们每次st[t]=true
的时候都要判断一下是否纳入了最小点集。
typedef pair<int, int> PII;
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
vector a(N,-1)
,但是效率相比于cstring
中的memset(a,-1,sizeof a)
直接处理一个字节一个字节的内存还是太慢了。path[N]
数组,path[i]
含义为:存储最短路径上,i
的前驱节点.直白点,就是在最短路径上i
这个节点是由哪个节点转移来的。单源最短路
,其实求出来的是一个节点到其他所有节点的最短路,基于上面额path数组我们就可以回溯一条从任意终点到起点的最短路径了。思路非常简单,核心为松弛操作。按边的长度来松弛,易知如果存在
n
个点,1~n
号点之间至多存在n-1
条边。也就是说如果我按边的长度来松弛至多只需要处理n-1
次,求得的值就是该点的最短路(假设存在)。内层循环就是遍历所有的边进行松弛操作就行(dist[b]>min(dist[b],dist[a]+weight))
。因此时间复杂度为O(nm)
struct Edge{
int a,b,w;
}edges[N];
dist[N]
备份成backup[N]
,松弛操作(dist[b]>min(dist[b],dist[a]+weight))
修改为dist[b]>min(dist[b],backup[a]+weight)
。请诸位思考,一开始所有点到起点之间的距离都是正无穷,dist[1]=0;此时起点自己到自己的距离为0。这时我利用边去更新dist即到起点的距离,是不是与起点相连的边都会被更新,并且由于backup数组的存在,本次更新的值并不会影响我后面值的更新,从而引发串联。如果将本次更新视为在当前层,那么我的backup
永远是用的上一层结果,从而保证这个算法一定是按距离起点边长为1,距离起点边长为2,… ,距离起点边长为n去更新的最短路。当然因为存在负权边,所以某些无穷大可能也会被更新为较小的无穷大,可以无视,因为这表示还是没有通路到起点。不
经过负环,对结果没有影响,可以当其不存在。建议使用memset和memcpy,属于cstring头文件的,对单个字节内存进行操作,效率高。
int n, m; // n表示点数,m表示边数
int dist[N]; // dist[x]存储1到x的最短路距离
int backup[N];
struct Edge // 边,a表示出点,b表示入点,w表示边的权重
{
int a, b, w;
}edges[M];
int bellman_ford(){
memset(dist,0x3f,sizeof dist);//这是每次调用这个算法都需要的初始化
dist[1]=0;//起点到自己的距离为0
for(int i=0;i<k;i++){//至多n-1次可以求出1~n路径上所有节点的最短路,至于多循环一次两次并没有什么影响,当然这个值也可能取决于题目。
memcpy(backup,dist,sizeof dist);
for(int j=0;j<m;j++){
int a=edges[j].a,b=edges[j].b,w=edges[j].w;
dist[b]=min(dist[b],backup[a]+w);//min在algorithm头文件中
}
}
if(dist[n]>0x3f3f3f3f/2) return -2e9;
else return dist[n];
}
//只需要记录n条边就行
for(int i=0;i<m;i++){
int x,y,z;
cin>>x>>y>>z;
edges[i]={x,y,z};
}
SPFA算法是对Bellman_Ford算法的优化,而Bellman_Ford算法时间复杂度最高的就是第二层循环中对所有边进行遍历更新
o(nm)
。但事实上处理a->b
边中dist[b]
是否更新取决它前面的a
节点也就是dist[a]
是否被更新,而dist[a]
又取决于它前一条更新它的边。简而言之,如果某一条边或者说某个节点被更新了,那么与这个节点相连的边是极有可能需要被更新的。
这样平均下来SPFA的算法时间复杂为o(m)。最坏情况下存在负环也要o(nm)
如果某一条边或者说某个节点被更新了,那么与这个节点相连的边是极有可能需要被更新的。
也就是实现上倾向于使用邻接表
,能把性能发挥到极致。队列queue
存储。一旦被更新了就存起来,后面再拿出来通过边权更新其他相连的节点,直到队列为空,就求出了所有节点的最短路了。dist
即可。因为队列中放的毕竟是节点编号,遍历处理的也是节点编号。#include
#include
#include
using namespace std;
const int N=1e5+10,M=1e5+10,INF=0x3f3f3f3f;
int n,m,idx;
vector<int> dist(N,INF);
vector<int> h(N,-1);
int e[M],w[M],ne[M];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa(){
//dist=vector(N,INF);再次初始化
queue<int> q;
dist[1]=0;
q.push(1);
st[1]=true;
while(q.size()){
auto t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[t]+w[i]){
dist[j]=dist[t]+w[i];
if(!st[j]) q.push(j),st[j]=true;
}
}
}
return dist[n]==INF?-INF:dist[n];
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t=spfa();
if(t==-INF) cout<<"impossible";
else cout<<t;
return 0;
}
spfa()
就会发现dist
没有被重新初始化。导致我在工程化项目中经常出现这种疏忽。。注意:以上我都是在说最短路的问题,并不涉及到负环。显然如果存在负环,那么负环路径上的点就会一直被更新,队列一直不空因此整个算法会陷入死循环。所以SPFA算法效率高、好写、还可以处理负权边,因此用的非常多。但缺点是无法和它老爹bellman_ford一样通过对边数的约束处理负环问题
抽屉原理
,加上一个cnt[N]
数组就可以解决负环问题。本质上和bellman_ford的思路是一样的。
Bellman_Ford:n个节点至多存在n-1条边。
SPFA:cnt[n]记录一下1~n之间的边数,如果负环的话cnt[j] = cnt[t] + 1;会不停更新,直到存在某个cnt[j]>=n,说明存在n条边,那就有n+1个节点,由抽屉原理可知一定有两个节点是相同的,也就是构成了环。
#include
#include
#include
using namespace std;
const int N=2010,M=10010,INF=0x3f3f3f3f;
int n,m,idx;
vector<int> dist(N,INF);
vector<int> h(N,-1);
int cnt[N];
int e[M],w[M],ne[M];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool spfa(){
dist=vector(N,INF);
queue<int> q;
for (int i = 1; i <= n; i ++ ) {
q.push(i);
st[i]=true;
}
while(q.size()){
auto t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[t]+w[i]){
dist[j]=dist[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>n-1) return true;
if(!st[j]) q.push(j),st[j]=true;
}
}
}
return false;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa()) cout<<"Yes";
else cout<<"No";
return 0;
}
弗洛伊德算法(Floyd-Warshall Algorithm)是一种用于解决图中节点之间最短路径问题的算法,它适用于有向图或者无向图,可以处理带有负权边但不包含负权回路的图。
弗洛伊德算法的核心思想是动态规划。它通过遍历所有节点对之间的可能路径,逐步更新从一个节点到另一个节点的最短距离,直到获得所有节点之间的最短路径为止。具体来说,算法维护一个二维数组D,其中D[i][j]表示从节点i到节点j的最短距离。然后通过以下递推关系来更新这些距离:D[i][j] = min(D[i][j], D[i][k] + D[k][j])
其中k表示所有可能的中间节点,如果从i到j经过k节点的路径比直接从i到j的路径更短,就更新D[i][j]的值。
弗洛伊德算法的时间复杂度为O(n^3),其中n为节点数,因此它适用于中等规模的图。该算法的优点是能够同时计算任意两点之间的最短路径,因此非常适合于需要多对多最短路径的场景。然而,对于大规模图来说,其时间复杂度可能会使其效率较低。
D[i][j] = min(D[i][j], D[i][k] + D[k][j])
即可。(k为中心节点、i,j为节点编号)不行!如果弄懂了这个问题,理论推导出公式我不敢说,但使用起来你将没有任何疑惑。
如果我们先遍历起点和终点,然后再遍历中间节点,就会出现某两个节点之间存在可能的最短路径,但由于中心节点尚未更新,这个最短路径可能被忽略。这样就会导致无法找到所有节点对之间的最短路径。
而当我们先遍历中间节点时,我们可以确保在考虑任意一对节点(i, j)时,中间节点已经被遍历过,从而能够考虑到经过这些中间节点的所有可能最短路径。通过遍历所有中间节点,我们最终能够找到所有节点对之间的最短路径,因此确保了算法的完备性。
D[i][j] = min(D[i][j], D[i][k] + D[k][j])
表达式,着眼的都是节点
。显然用邻接矩阵
比较好,方便遍历所有节点以及获得任意两节点之间的关系。const int INF=0x3f3f3f3f;
初始化:
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
最后判断是否d[a][b]间存在最短路,会出现和Bellman_Ford算法一样的问题,因为二者归根结底还是遍历了所有边,甚至Floyd暴力程度连不存在的边也会处理一下,导致如果存在负权边,某些距离无穷大的节点会被更新成较小的无穷大
,这是需要看下数据的范围全为负值叠一起加个正无穷看下有多大。一般经验,d[a][b]>INF/2就可以认为是不存在最短路了。
最小生成树是指
在一个加权连通图中找到一个包含所有顶点的树,并且使得树的边的权值之和最小
。换句话说,给定一个带权的无向连通图,最小生成树是指一个边的子集,它是一棵树,包含图中的所有顶点,使得这棵树的所有边的权值之和达到最小。最小生成树常常用于解决诸如网络设计、电路设计、城市规划等领域的问题,以确保连接所有节点的同时,尽可能减少总成本或总距离。经典的算法包括Prim算法和Kruskal算法,它们可以有效地找到给定图的最小生成树。
和
Dijkstra求最短路
的思路算法非常相似,最大的区别在于Dijkstra
使用dist[N]数组存储节点到起点的距离
。而Prim算法
使用dist[N]存储节点到最小生成树集合的距离
。因为最小生成树着眼的是连通图的权值和最小,最小生成树内部本就是联通的、权值和最小的。此时我们想纳入新的节点到最小生成树中,就是要寻找与最小生成树相连的、边权最小的那个节点。然后每纳入一个节点都需要利用这个节点更新一下相连的边到最小生成树集合的位置了,因为有新的节点纳入最小生成树了,与这个节点相连的节点到最小生成树集合的距离可能需要修改遍历一遍就行,更小就修改一下,修改后的值就是这两点的边权。
int n; // n表示点数
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
memset(dist, 0x3f, sizeof dist);
int res = 0;
for (int i = 0; i < n; i ++ )
{
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if (i && dist[t] == INF) return INF;
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
if (i && dist[t] == INF) return INF;
if (i) res += dist[t];
i
的判断,是因为memset(dist, 0x3f, sizeof dist);
一开始我们将所有的dist[N]
都初始化为正无穷,并没有将任何一个点纳入最小生成树集合,因此会出现第一次循环时任何一个点到最小生成树集合的距离都为正无穷。需要进行对i=0
也就是第一次循环进行特判,允许第一次循环碰到dist[t]==INF
,如果后面再碰到最小生成树集合存在元素
且当前dist[t]==INF
那就说明这个点是孤立的,无法构成最小生成树。#include
#include
#include
#include
using namespace std;
const int N=510,M=1e5+10,INF=0x3f3f3f3f;
int h[N],e[2*M],w[2*M],ne[2*M],idx;
int n,m;
bool st[N];
typedef pair<int,int> PII;
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int Prim(){
int cnt=0,res=0;
priority_queue<PII,vector<PII>,greater<PII>> q;
q.push({0,1});
while(q.size()){
auto t=q.top();
q.pop();
int weight=t.first,ver=t.second;
if(st[ver]) continue;
st[ver]=true;
res+=weight;
cnt++;
for(int i=h[ver];i!=-1;i=ne[i]){
int j=e[i];
if(st[j]) continue;
q.push({w[i],j});
}
}
if(cnt==n) return res;
else return -INF;
}
int main()
{
// ios::sync_with_stdio(false),cin.tie(0);
memset(h,-1,sizeof h);
cin>>n>>m;
while(m--){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
int t=Prim();
if(t==-INF) cout<<"impossible";
else cout<<t;
return 0;
}
所有边
进行降序
排序 O(mlogm)
并查集
int p[N];//记录所有节点的父节点
for(int i=1;i<=n;i++) p[i]=i;
//每一个节点的父节点都是自己,即一开始所有节点都是孤立
//路径压缩的并查集,返回某个节点的祖宗节点。近乎O(1)
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
STL sort()
,但要实现com比较函数
struct Edge{
int a,b,w;
bool operator <(const Edge &edge){
return w<edge.w;
}
}edges[N];
//C++兼容了C的结构体,将其看作一个类即可,重载了一下 <运算符
int res=0;cnt=0;
for(int i=1;i<=n;i++){
int a=edges[i].a,b=edges[i].b,w=edge[i].w;//此时a,b为该边的左右端点
a=find(a),b=find(b);//此时a,b被更新为a,b这两个端点所属集合的祖宗节点编号。
if(a!=b){
res+=w;//记录最小生成树权值
cnt++;//记录最小生成树中的边数
p[a]=b;//将a的父节点设置为b节点编号(相当于将a这个集合插入到了b这个集合中)
}
}
if(cnt<n-1) 无法构成最小生成树
二分图(Bipartite Graph)是一种特殊的图论结构,其中所有的顶点可以被分成两个互不相交的集合,使得每一个边的两个端点都分别来自这两个不同的集合。也就是说,图中的任意一条边都连接着两个集合中的两个顶点。二分图不含奇数环,否则会出现同一个点染上两种颜色(即同一个点分别位于两个集合)
选择一个起始顶点,将其染色。
对于每一个与已经染过色的顶点相邻的未染色顶点,将其染成与相邻顶点不同的颜色。
如果在染色过程中遇到一个已经染过色的顶点,需要检查其颜色是否与我们打算给它的颜色相同。如果不同,那么说明这个图不能被有效地染成两种颜色,因此它不是一个二分图。
#include
#include
using namespace std;
const int N=1e5+10,M=2*N;
int n,m;
int color[N];
vector<int> h(N,-1);
int idx,e[M],ne[M];
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
bool bfs(int ver){
int q[N],hh=0,tt=-1;
color[ver]=1;
q[++tt]=ver;
while(hh<=tt){
auto t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(!color[j]){
color[j]=3-color[t];
q[++tt]=j;
}else if(color[j]==color[t]) return false;
}
}
return true;
}
int main()
{
cin>>n>>m;
while(m--){
int a,b;
cin>>a>>b;
add(a,b),add(b,a);
}
bool flag=true;
for(int i=1;i<=n;i++){
if(!color[i]){
if(!bfs(i)){//bfs()的含义就是进行二分染色,成功就返回true,否则false
flag=false;
break;
}
}
}
if(flag) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == -1)
{
if (!dfs(j, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check()
{
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, 0))
{
flag = false;
break;
}
return flag;
}
匈牙利算法,又称为Kuhn-Munkres算法或者KM算法,是一种用于解决分配问题的优化算法。
求二分图最大匹配数
#include
#include
#include
using namespace std;
const int N=510,M=1e5+10;
//采取什么方式建图更多的是取决于时间复杂度和我们到底在算法处理过程中需要什么样的数据
//实际应用中二分图大多是无向图,但这里不需要存u->v和v->u的边,假设我们从u开始遍历这个集合求二分图最大匹配
//那么我们每次组要处理的是u中点的出边,整个算法流程中并不涉及从v->u的边,因此只要存储u->u的边即可。
int h[N],e[M],ne[M],idx;
int n1,n2,m;
int match[N];//存储v集合中被匹配的点,对应点是u集合中的哪个
bool st[N];//当u对应点放弃当前匹配去尝试寻找下一个,最重要的作用就是避免在第一个点的时候无限递归下去
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
//如果x节点能找到一个匹配就返回true
bool find(int x){
for(int i=h[x];i!=-1;i=ne[i]){
int j=e[i];
if(!st[j]){
st[j]=true;
if(match[j]==0||find(match[j])){
match[j]=x;//最核心的一步,找到了合适的别忘了修改
return true;
}
}
}
return false;
}
int main()
{
memset(h,-1,sizeof h);
cin>>n1>>n2>>m;
while(m--){
int u,v;
cin>>u>>v;
add(u,v);
}
int res=0;
for(int i=1;i<=n1;i++){
memset(st,0,sizeof st);
if(find(i)){
res++;
}
}
cout<<res;
return 0;
}