这周专题是图论:
目录
这周专题是图论:
树和图的存储及遍历:
存储:
遍历:(每个点只遍历一次,所以需要卡一个布尔数组标记是否已经被遍历)
1.深度优先遍历:
树的重心:
2.宽度优先遍历:
图中点的层次:
拓扑序列(有向图才会有拓扑序列):
最短路:
朴素版Dijkstra:
堆优化版本的Dijkstra;
Bellman-Ford算法:
树和图的存储及遍历:
树是特殊的图:无环且连通,所以只讲图;
图分为有向图和无向图,可以把无向图看成特殊有向图;
有向图如何存储:
存储:
一般有两种存储方式:
- 邻接矩阵g[a][b],如果有权重就将权重赋值给数组,如果没有权重就是布尔值,这条边存不存在。(不能存储重边)
- 邻接表(用的最多):每个点都有一个单链表,存储从这个点走向那个点。
举例:
当我们想要插入一条新的边:
比如新加一条2到3;
找到2的单链表然后把3插到里面去:
一般选在头的位置插
#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=N*2;
int h[N];//N个链表的链表头
int e[M];//存的所有的边
int ne[M];//每一个节点的next节点;
int idx;
void add(int a,int b){//将节点b插入到节点a,比如新增边:2-3;
e[idx]=b;//将节点3的值附上
ne[idx]=h[a];//看图就是绿色的边
h[a]=idx++;//看图是红色的边
//idx表示当前节点已经用了多少个节点
//或者说可以理解成每个节点分配的唯一标识序号
}
int main(){
memset(h,-1,sizeof(h));
return 0;
}
我觉得这里的idx就是建立一个新的“节点”,将要插入的点赋值给这个新节点,然后将这个节点的下一个值就是ne[idx]接到要插入的节点的头节点的第一个值,就是绿色边,然后再将被插入节点的头节点接到 插入进来的节点,就是下图的红色边。然后为了下一次新边的加入,就预先再创造一个新节点等待赋值。
遍历:(每个点只遍历一次,所以需要卡一个布尔数组标记是否已经被遍历)
时间复杂度是O(n+m);
1.深度优先遍历:
#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=N*2;
int h[N];//N个链表的链表头
int e[M];//存的所有的边
int ne[M];//每一个节点的next节点;
int idx;
bool st[N];
void add(int a,int b){//将节点b插入到节点a,比如新增边:2-3;
e[idx]=b;//将节点3的值附上
ne[idx]=h[a];//看图就是绿色的边
h[a]=idx++;//看图是红色的边
}
void dfs(int u){
st[u]=1;//标记一下这个点已经被搜索过了。
for(int i=h[u];i!=-1;i=ne[i])//遍历一下u的所有出边
{
int j=e[i];
if(!st[j])dfs(j);
}
}
int main(){
memset(h,-1,sizeof(h));
dfs(1);
return 0;
}
树的重心:
定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中的最大值最小,那么这个节点被称为树的重心。
(树的重心可能不唯一)
举个例子:
删掉1节点,连通块的最大值是4
删掉2的连通块的最大值是6
删掉4,连通块最大值是5
其余的边就不枚举了
所以树的重心是1,最优解是4
846. 树的重心 - AcWing题库
用树的深度优先遍历。
怎么算?以删除4为例子:
#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=N*2;
int h[N];//N个链表的链表头
int e[M];//存的所有的边
int ne[M];//每一个节点的next节点;
int idx;
int n,s,ans=N;
bool st[N];
void add(int a,int b){//将节点b插入到节点a,比如新增边:2-3;
e[idx]=b;//将节点3的值附上
ne[idx]=h[a];//看图就是绿色的边
h[a]=idx++;//看图是红色的边
}
//返回
int dfs(int u){//以u为根的子树的大小
st[u]=1;//标记一下这个点已经被搜索过了。
int sum=1,res=0;
for(int i=h[u];i!=-1;i=ne[i])//遍历一下u的所有出边
{
int j=e[i];
if(!st[j]){
int s=dfs(j);//当前子树的大小,也算一个连通块
res=max(res,s);//当前以儿子为根节点的子树是以u为根节点的子树的一部分
sum+=s;
}
}
res=max(res,n-sum);
ans=min(ans,res);
return sum;
}
int main(){
cin>>n;
memset(h,-1,sizeof(h));
for(int i=0;i<n-1;i++){
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
}
dfs(1);
cout<<ans<<endl;
return 0;
}
2.宽度优先遍历:
847. 图中点的层次 - AcWing题库
图中点的层次:
#include<bits/stdc++.h>
using namespace std;
const int N=100005,M=2*N;
int e[M],ne[M],h[N],idx;
int d[M],q[N],n,m;
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
int bfs(){
int hh=0,tt=0;//队头和队尾
q[0]=1;
memset(d,-1,sizeof(d));
d[1]=0;//最开始只有第一个被遍历过了
while(hh<=tt){
int t=q[hh++];
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(d[j]==-1){
d[j]=d[t]+1;
q[++tt]=j;//数组模拟队列的写法
}
}
}
return d[n];
}
signed main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
}
cout<<bfs()<<endl;
return 0;
}
图的宽搜遍历的经典应用:
拓扑序列(有向图才会有拓扑序列):
AcWing 848. 有向图的拓扑序列 - AcWing
什么是拓扑序列:对于图中的每条边 (x,y),x 在 A中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
举例:
所有的边都是从前指向后的
一个有向无环图一定存在一个拓扑序列,所以一个有向无环图也可以称为一个拓扑图
所有入度为0的点都可以排在最前面的位置,(一个无环图至少存在一个入度为0的点)
后面就是一个宽搜的过程
#include<bits/stdc++.h>
using namespace std;
const int N=100005,M=2*N;
int e[M],ne[M],h[N],idx;
int d[M],q[N],n,m;
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
bool topsort(){
int hh=0,tt=-1;
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]==0)q[++tt]=j;
}
}
return tt==n-1;
}
signed main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));
for(int i=1;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;
}
最短路:
难点在建图:
朴素版Dijkstra:
AcWing 849. Dijkstra求最短路 I - AcWing
#include<bits/stdc++.h>
using namespace std;
const int N=510;
int n,m;
//cin>>n>>m;
int g[N][N];
int dist[N];
bool st[N];
int dijkstra(){
memset(dist,0x3f,sizeof(dist));
dist[1]=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不是最短的;
t=j;
}
}
st[t]=1;
for(int j=1;j<=n;j++){
dist[j]=min(dist[j],dist[t]+g[t][j]);
}
}
if(dist[n]==0x3f3f3f3f)return -1;
return dist[n];
}
int main(){
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof(g));
while(m--){
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);
}
//g[a][b]=min(g[a][b],c);
int t=dijkstra();
printf("%d\n",t);
return 0;
}
稠密图用邻接矩阵,
稀疏图用邻接表;
注意:
在这里数组初始化用memset特别要注意:
给距离初始化的时候;
当我们用0x3f给距离数组赋值的时候,赋值的结果却是0x3f3f3f3f
memset函数初始化特点是按字节去逐个初始化
所以,一般只用来初始化0,-1,0x3f这几个数字。其他的则建议使用for循环来初始化。
其他数字转变的过程如下图:
如果将数组初始化为正无穷的时候,用到了memset,要用0x3f,最后如果验证时候为正无穷的时候要等于的是0x3f3f3f3f;
注意用memset初始化为正无穷的时候,不能用define int long long;
d[n]: 4557430888798830399
0x3f3f3f3f: 1061109567
堆优化版本的Dijkstra;
用优先队列找到距离最近的点
AcWing 850. Dijkstra求最短路 II - AcWing
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define PII pair<int,int>
vector<PII>mp[150005];
int n,m;
int dist[150001];
bool st[155001];
void djstl(){
for (int i = 1; i <=n ; ++i) {//举例数组初始化为无穷大
dist[i]=(int)1e15;
}
priority_queue<PII,vector<PII>,greater<PII>>q;//定义优先队列
dist[1]=0;//起点的距离是0
q.push({0,1});//将起点放入队列中,要对距离排序,所以距离在第一位
while(!q.empty()){//如果队列不为空
auto t=q.top();//将距离最小的点弹出
q.pop();
int ver=t.second,ds=t.first;//最上面的点标号是ver,距离是ds;
if(st[ver])continue;//如果该点已经确定了最短路,就继续找下一个最近的点
st[ver]= true;//标记
for (int i = 0; i <mp[ver].size() ; ++i){//找以ver为出边所连接的点
auto p=mp[ver][i];//枚举所有他所连接的点;
if(dist[p.first]>ds+p.second){//如果1到该点的距离大于(已经确定最短路的点到该点的距离加上最短路的距离)
dist[p.first]=ds+p.second;//更新该点的最短距离
q.push({dist[p.first],p.first});//将已经找到最短路的点放入队列中;
}
}
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i = 0; i <m ; ++i) {
int x,y,z;
cin>>x>>y>>z;
mp[x].push_back({y,z});//读入边
}
djstl();//进行最短路
if(dist[n]==(int)1e15)cout<<"-1";
else
cout<<dist[n];
return 0;
}
例题:旅行
1004-旅行_2021秋季算法入门班第九章习题:图论(重现赛)@IR101 (nowcoder.com)
有n个点,选择3个点让这三个点之间最短路径最大:
样例1:答案是422
样例二:答案是三
答案是0
样例三:
样例4:答案是九
稀疏图,我们可以用邻接表存图或则链式向前星
然后我们计算以每个点为起点的最短路,将起点作为我们要选取的三个点中的中间点,以该起点,找到对应的三点中作为起点和终点的点。
简单说就是枚举每个点当作中间点,求出由当前点到其他任意两点最长的两个路径。
是稀疏图且是正边,那么我们可以使用堆优化版的Dijkstra,
对每一个点跑一遍Dijkstra
#include<bits/stdc++.h>
using namespace std;
// 定义常量
const int N = 1005, INF = 0x3f3f3f3f;
// 定义边和节点的结构体,用于Dijkstra算法中的优先队列
struct node{
int x, d; // x是节点编号,d是从起点到该节点的距离
bool operator< (const node& a) const // 重载小于运算符,使得优先队列按照距离d的降序排列
{
return d > a.d;
}
};
// 用于存储最短路径的数组
int dis[N];
// 图的邻接表表示
int Next[2*N], e[2*N], head[N], val[2*N]; // 因为无向图,每条边存储两次
int fg[N] = {0}; // 标记数组,用于Dijkstra算法中判断节点是否已经被访问过
int n, m, cnt = 0; // n是节点数,m是边数,cnt用于记录边的数量
// 添加边到图的邻接表中
void add(int x, int y, int z)
{
e[cnt] = y;
val[cnt] = z;
Next[cnt] = head[x];
head[x] = cnt++;
}
// Dijkstra算法实现,用于计算从x出发到所有其他节点的最短路径
int Dijkstra(int x)
{
priority_queue<node> q; // 使用优先队列优化Dijkstra算法
memset(dis, INF, sizeof(dis)); // 初始化距离数组为正无穷
memset(fg, 0, sizeof(fg)); // 初始化访问标记数组
dis[x] = 0; // 起点到自身的距离为0
q.push({x, 0}); // 将起点加入优先队列
while(!q.empty())
{
node k = q.top();
q.pop();
if(fg[k.x]) continue; // 如果节点已经被访问过,则跳过
fg[k.x] = 1; // 标记节点为已访问
// 遍历当前节点的所有邻接节点
for(int i=head[k.x]; i!=-1; i=Next[i])
{
if(dis[e[i]]>dis[k.x]+val[i]) // 如果找到更短的路径
{
dis[e[i]] = dis[k.x] + val[i]; // 更新距离
q.push({e[i], dis[e[i]]}); // 将邻接节点加入优先队列
}
}
}
// 对所有可达节点的距离进行降序排序,找到最远的两个距离
sort(dis+1, dis+n+1, greater<int>());
int kk = 0, sum = 0;
for(int i=1; i<n && kk<2; i++) // 遍历排序后的距离数组,找到前两个非无穷大的距离
if(dis[i]!=INF) sum += dis[i], kk++;
return (kk<2)? -1:sum; // 如果找不到两个可达的节点,则返回-1;否则返回两个最远距离的和
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t;
cin >> t; // 读取测试用例数量
while(t--)
{
int maxv = -1; // 初始化最大值为-1
cnt = 0; // 重置边的计数器
memset(head, -1, sizeof(head)); // 初始化邻接表头指针
// 注意:这里不应该重置e, val, Next数组,因为它们会被连续使用
cin >> n >> m; // 读取节点数和边数
// 读取边并添加到图中
for(int i=0; i<m; i++)
{
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
add(y, x, z); // 因为是无向图,所以边要添加两次
}
// 枚举每个节点作为中转点,计算最大距离和
for(int i=1; i<=n; i++) maxv = max(maxv, Dijkstra(i)); //逐一枚举中转点
cout << maxv << endl;
}
return 0;
}
时间复杂度是n*mlogn
Bellman-Ford算法:
适用有负权边的图,限制边数;
如果有负权回路,最短路不一定存在,最短路可能为负无穷;
所以一般情况下:有最短路就不会存在负权回路
853. 有边数限制的最短路 - AcWing题库
迭代n次,每一次循环所有边(a,b,w 从a到b权重是w);
(迭代k次,表示从一号点经过不超过k条边走到每个点的最短距离);
这个算法存边随便存,一般定义一个结构体数组
struct nod{
int a,b,w;
}edge[M];
遍历更新dist[b]=min(dist[b],dist[a]+w);
循环遍历n次后,对于所有的边:
dist[b]<=dist[a]+w;(三角不等式)
更新的过程叫:松弛操作;
memcpy(backup,dist,sizeof(dist));
这个是把dist数组备份一下;
举一个例子:
枚举所有边的时候可能发生串联
会无视边的限制
保证更新点时候时候是上一次迭代的结果,就得给dist数组备份;所以backup数组存的就是上一次迭代的结果
if(dist[n]>0x3f3f3f3f/2){
flag=1;
}
这里为什么大于一个0x3f3f3f3f/2?;
因为一号点到不了n号结点,但是中间的某个结点能到达n;
但是此时中间的某个节点是0x3f3f3f3f,在走到终点的时候可能走的都是负权边,但是题目有限制不会减掉太多,所以最后的答案必定小于0x3f3f3f3f,但是此时却也是到达不了的情况,所以做了除以2的处理
#include<bits/stdc++.h>
using namespace std;
const int M=100005;
int n,m,k,flag;
struct Edge{
int a,b,w;
}edge[M];
int dist[505];
int backup[M];
int bellman_ford(){
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
for(int i=0;i<k;i++){//不超过k条边的最短路径
memcpy(backup,dist,sizeof(dist));
for(int j=0;j<m;j++){
int a=edge[j].a,b=edge[j].b,w=edge[j].w;
dist[b]=min(dist[b],backup[a]+w);
}
}
if(dist[n]>0x3f3f3f3f/2){
flag=1;
}
return dist[n];
}
signed main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=0;i<m;i++){
int a,b,w;
cin>>a>>b>>w;
edge[i]={a,b,w};
}
int t=bellman_ford();
if(flag)puts("impossible");
else cout<<t<<endl;
return 0;
}
SPFA算法:
最短路
是对Bellman-Ford的一种优化;
要想更新dist[b],那么就得dist[a]变小;
用宽搜来做优化
将起点放入队列;
只要队列不为空:(只要队列里边还有节点变小的话)
1.将队头取出;t=q.front();q.pop();
2.更新t的所有出边;(如果t变小,所有以t为起点的终点都有可能变小;) t -_w_->b;
3.将b放入队列;
#include<bits/stdc++.h>
using namespace std;
const int M=100005;
int h[M];
int e[M];
int w[M];
int idx;
int f;
int ne[M];
int dist[M];
int st[M];
void add(int a,int b,int ww){
e[idx]=b;
w[idx]=ww;
ne[idx]=h[a];
h[a]=idx++;
}
void spfa(){
memset(dist,0x3f,sizeof(dist));
dist[1]=0;
queue<int>q;
q.push(1);
st[1]=1;//标记起点已经被访问过了
while(!q.empty()){
auto p=q.front();
q.pop();
st[p]=0;//表示这个点已经不在队列里了
for(int i=h[p];i!=-1;i=ne[i]){//访问以p点为起点的终点
int j=e[i];//去点
if(dist[j]>(dist[p]+w[i])){//如果起点的距离加上边权小于终点距离就更新他;
dist[j]=dist[p]+w[i];
if(!st[j]){//如果该终点还没有被更新过,放入队列并且标记为以被更新过;
q.push(j);
st[j]=1;
}
}
}
}
}
signed main(){
int n,m;
cin>>n>>m;
memset(h,-1,sizeof(h));
for(int i=1;i<=m;i++){
int a,b,ww;
cin>>a>>b>>ww;
add(a,b,ww);
}
spfa();
if(dist[n]==0x3f3f3f3f)cout<<"impossible"<<endl;
else cout<<dist[n];
return 0;
}
感觉代码形式和dijkstra算法很像,那他们的区别在哪里呢?
dijkstra算法:
它是每次找到离原点最近的点,放入堆中(成为堆顶)并且标记,再以这个点为起点去更新与它相连的点,
SPFA算法:
它是要对所有的边去进行一次松弛操作;
进行了n-1次更新,先初始化dis数组,起点赋值为0,其余赋值为无穷大;
先起点入队列,入了队列的被标记,当队列不为空时循环,队首元素出队,松弛与队首元素相连的边,这些被更新的点如果不在队列中就加入队列, 再次队首元素出队;
松弛与队首元素相连的边,它是不需要去找离原点最近的点的;
所以Dijkstra算法用的是小根堆优化;
SPFA直接用的队列;
判断是否有负环
852. spfa判断负环 - AcWing题库
判断负环
就是spfa最短路多加了一个cnt[M]数组;
要将所有点都入队,因为不是每个点都能到达负环,所以得从每一个点都出发一遍
然后就是dist初始化可以删掉,详细细节见代码:
#include<bits/stdc++.h>
using namespace std;
const int M=100005;
int h[M];
int e[M];
int w[M];
int idx;
int cnt[M];
int f;
int n,m;
int ne[M];
int dist[M];
int st[M];
void add(int a,int b,int ww){
e[idx]=b;
w[idx]=ww;
ne[idx]=h[a];
h[a]=idx++;
}
bool spfa(){
queue<int>q;
for(int i=1;i<=n;i++){
q.push(i);
}
while(!q.empty()){
auto p=q.front();
q.pop();
st[p]=0;//表示这个点已经不在队列里了
for(int i=h[p];i!=-1;i=ne[i]){//访问以p点为起点的终点
int j=e[i];//去点
if(dist[j]>(dist[p]+w[i])){//如果起点的距离加上边权小于终点距离就更新他;
dist[j]=dist[p]+w[i];
cnt[j]=cnt[p]+1;
if(cnt[j]>=n)return 1;
if(!st[j]){//如果该终点还没有被更新过,放入队列并且标记为以被更新过;
q.push(j);
st[j]=1;
}
}
}
}
return 0;
}
signed main(){
cin>>n>>m;
memset(h,-1,sizeof(h));
for(int i=1;i<=m;i++){
int a,b,ww;
cin>>a>>b>>ww;
add(a,b,ww);
}
//spfa();
if(spfa())cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
Flord算法:
有三重循环,
for(int k=1;k<=n;k++){
for(int i=1;i<n;i++){
for(int j=1;j<=n;j++){
d[i][k]=min(d[i][j],d[i][k]+d[k][j]);
}
}
}
循环结束之后就是d[i][j]就是从 i 到 j 的最短路;
#include<bits/stdc++.h>
using namespace std;
int d[210][210];
int i,j,v,w,u;
int n,m,k;
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]);
}
}
}
signed main(){
cin>>n>>m>>k;
memset(d,0x3f,sizeof d);
for(i=1;i<=n;i++) d[i][i]=0;
while(m--){
cin>>u>>v>>w;
d[u][v]=min(d[u][v],w);
}
floyd();
while(k--){
int x,y;
cin>>x>>y;
if(d[x][y]>0x3f3f3f3f/2) cout<<"impossible";
else cout<<d[x][y];
cout<<'\n';
}
}
常用代码模板3——搜索与图论 - AcWing
最小生成树
最小生成树对应的图都是无向图
朴素版prim:
858. Prim算法求最小生成树 - AcWing题库
1.初始化距离dist[N];将所有距离初始化为正无穷
2.n次迭代,每次迭代:
a.在当前连通块当中的所有点——>s;
找到集合外距离最近的点,赋值给t。
用t更新其他点到集合的距离
把t加到集合当中去(s[t]=1);
#include <iostream>
#include <cstring>
using namespace std;
const int N=510;
bool st[N]; //将生成树看成一个集合,在集合中即为true,不在即为false
int dist[N],g[N][N]; //dist数组是点到该集合的距离,g数组是两点之间的距离
int n,m,res;
int prim()
{
memset(dist,0x3f,sizeof(dist));
for(int i=0;i<n;i++) //因为最小生成树要加入所有点,故要遍历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; //选出点后便加入最小生成树集合
if(i&&dist[t]==0x3f3f3f3f) return dist[t]; //如果某点不是第一个加入最小生成树中的点,且该点的值为初始化的0x3f3f3f3f,即该点不能与其他点相连,故不能形成最小生成树
if(i) res+=dist[t]; //如果该点不是第一个点,则将该点与最小生成树集合之间的边的权重加入结果中
for(int j=1;j<=n;j++) dist[j]=min(dist[j],g[t][j]); //每次最小生成树集合中加入新的点后,都更新其他点与集合的距离,本质上是更新未加入集合的点,但由于特判增加复杂度,故直接遍历所有点
}
return res; //若未发现不与集合连通的点,则返回点之间的边权重之和
}
int main()
{
memset(g,0x3f,sizeof(g));
cin>>n>>m;
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b]=g[b][a]=min(g[a][b],c);
}
int t=prim();
if(t==0x3f3f3f3f) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}
Kruskal算法:
859. Kruskal算法求最小生成树 - AcWing题库
最开始的时候没有边
1.将所有边按权重从小到大排序O(mlogn);
2.枚举每条边a,b,权重是c。枚举的时候
3.如果当前的a,b不连通的话,将这条边加入集合中(并查集);
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int>PII;
const int N=1e5+5,M=2e5+5,INF=0x3f3f3f3f,Mod=10007;
const double eps=1e-8;
typedef long long ll;
int n,m,d[N],fa[N];
struct E{
int a,b,c;
bool operator< (const E &W)const{
return c<W.c;
}
}ed[M];
int find(int x){
if(fa[x]!=x)fa[x]=find(fa[x]);
return fa[x];
}
int kruscal(){
sort(ed,ed+m);
for(int i=1;i<=n;++i)fa[i]=i;
int res=0,cnt=0;
for(int i=0;i<m;++i){
int a=ed[i].a,b=ed[i].b,c=ed[i].c;
a=find(a),b=find(b);
if(a!=b){
res+=c;
fa[a]=b;
cnt++;
}
}
if(cnt<n-1)return INF;
return res;
}
int main(){
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=0;i<m;++i){
cin>>ed[i].a>>ed[i].b>>ed[i].c;
}
int res=kruscal();
if(res==INF)cout<<"impossible";
else cout<<res;
return 0;
}
二分图
一个图数二分图当且仅当图中不含奇数环(环当中边的数量是奇数)
染色法
由于图中不含奇数环所以染色过程中一定没有矛盾
如果有矛盾就不是二分图;
染色法从前往后遍历:
深度优先遍历;
if(i未被染色) dfs(i,颜色);
AcWing 860. 染色法判定二分图 - AcWing
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+3;
int n,m;
int i,j,u,v;
int cl[N];
struct edge{
int v;
};
vector<edge>ed[N];
bool dfs(int u,int c)
{
cl[u]=c;
for(int i=0;i<ed[u].size();i++)
{
int v=ed[u][i].v;
if(!cl[v])
{
if(!dfs(v,3-c)) return false;
}
else if(cl[v]==c) return false;
}
return true;
}
signed main()
{
cin>>n>>m;
memset(cl,0,sizeof cl);
while(m--)
{
cin>>u>>v;
ed[u].push_back({v});
ed[v].push_back({u});
}
bool f=true;
for(i=1;i<=n;i++)
{
if(!cl[i])
{
if(!dfs(i,1)){
f=false;
break;
}
}
}
if(f) cout<<"Yes";
else cout<<"No";
}
匈牙利算法:
匈牙利算法:男女相亲,男选女可占可让,贪心配对:
1.枚举n个男生,
每轮vis[]初始化为0,(即女生皆可选),
深搜若能配成对,ans+1;
2.枚举男生u的心仪女孩v,
(1)若女孩已标记就逃过
(2)若女孩没男朋友,则配成对
若女孩的男友可以让出,则配成队。
(3)否则,枚举u的下一个心仪女孩
3.枚举完u的心仪女孩都不能配成对,则return 0;
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,k,m,a,b,ans;
#define M 10000
#define N 200005
struct edge{
int v,next;
}e[M];
int h[N],idx;
int vis[N],match[N];
void add(int a,int b){
e[++idx]={b,h[a]};
h[a]=idx;
}
bool dfs(int u){
for(int i=h[u];i;i=e[i].next){
int v=e[i].v;//枚举该男孩的心动女孩
if(vis[v])continue;
vis[v]=1;//先标记这个女孩
if(!match[v]/*如果该女孩没有对象*/||/*枚举此女孩的男友还有没有可选的女孩*/dfs(match[v])){
match[v]=u;//配成对
return 1;
}
}
return 0;
}
signed main(){
cin>>n>>m>>k;
for(int i=0;i<k;i++){
cin>>a>>b;
add(a,b);
}
for(int i=1;i<=n;i++){//男选女,枚举每一个男生
memset(vis,0,sizeof(vis));
if(dfs(i))ans++;//走进男生i;
}
cout<<ans<<endl;
}
“见面吗?
如果答案是不可以,
那我就等会再问一遍 (⊂(・▽・⊂)”