数据结构与算法分析-C++描述 第9章 图

1.图

图(graph):顶点(vertex)和边(edge)组成的集合。
边:点对(v,w),如果点对有序,则称该图为有向图(digraph),否则叫无向图。
邻接(adjacent):点v和点w邻接当且仅当(v,w)是边。
权(weigh)或值(cost):边的属性。
路径(path):w_1,w_2,...,w_N的顶点序列,其中(w_i,w_{i+1})是边,这样一条路径的长(length)是该路径的边数N-1,不包含边的路径长度为0,路径(v,v)叫做环。
简单路径:路径上的所有顶点互异,但路径的起点和终点可以相同。
回路(cycle):满足w_1=w_N且长至少为1的一条路径,如果回路是简单路径,则称之为简单回路。
如果一个有向图没有回路,则称其为有向无环图(DAG),有向无环图一定没有双向边。
对于一个无向图,如果任取它的两个顶点,总存在一条路径,则称这个无向图为连通图(connected)。
对于一个有向图,如果任取它的两个顶点,总存在一条路径,则称这个有向图为强连通图(strongly connected)。
如果一个有向图不是强连通的,但是它的基础图(underlying graph),即对应的无向图连通,则它是弱连通图(weakly connected)。
完全图(complete graph):每一对顶点之间都存在一条边的图。

2.图的表示

邻接表:对每个顶点,用一个表存放所有邻接的顶点。
实现:开个vector<int> a[size], size为点的个数。
如果要保存顶点的名字或者权,需要用map<struct XX,struct XX> a[size]

3.拓扑排序

对于有向无环图,如果有一条v到w的路径,则把顶点v排在w的前面。
顶点v的入度(indegree):边(u,v)的条数。
排序方法:先遍历邻接表,让所有入度为0的顶点进入队列,让队首元素出队并将它和它的边一起删除,即它指向的顶点入度都减1,重复以上步骤直到队列为空,出队的顺序即为拓扑排序顺序。

4.最短路径算法

加权路径长:从v_1v_n的路径中,所有边的权的加和。
无权路径长:从v_1v_n的路径边数,即n-1
单源最短路问题:给定一个加权图G和一个顶点s,找出从s到G中的其他顶点的最短加权路径。这里先假定权都是正的,否则反复走负边可能会使加权路径长无限小。从s到s的最短路径为0。
(1)无权最短路:
直接BFS:从s开始,将s记为0,把所有与s邻接的顶点记为1,然后不断扩展邻接的顶点的最短路径为原来顶点加1,直到所有顶点都算过了。
实现:
第一行输入m,n为顶点数和边数。
以下的n行各有两个整数,为有向边。
输入一个整数x。
输出第x个顶点到每个顶点的最短路径长以及最短路径,如果到不了,输出-1.

#include<iostream>
#include<vector>
#include<queue>
#include<string>
#include<sstream>
using namespace std;
int m, n;
struct vertex {
    int num;
    int dist;
    int from;
}v[100];
vector<int> edge[1000];
queue<vertex> q;
int a, b, x;
int len;
int num;
int main() {
    cin >> m >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a >> b;
        edge[a].push_back(b);
    }
    cin >> x;
    for (int i = 1; i <= m; i++) {
        v[i].num = i;
        v[i].dist = -1;
    }
    v[x].dist = 0;
    v[x].from = -1;
    q.push(v[x]);
    while (!q.empty()) {
        vertex now = q.front();
        q.pop();
        num = now.num;
        len = edge[num].size();
        for (int i = 0; i < len; i++) {
            if (v[edge[num][i]].dist == -1) {
                v[edge[num][i]].dist = v[num].dist + 1;
                v[edge[num][i]].from = num;
                q.push(v[edge[num][i]]);
            }
        }
    }
    int temp;
    stringstream ss;
    string out;
    for (int i = 1; i <= m; i++) {
        cout << v[i].dist << endl;
        if (v[i].dist == -1) continue;
        cout << i;
        temp = i;
        ss.clear();
        out.clear();
        while (v[temp].from != -1) {
            ss << "<-" << v[temp].from;
            temp = v[temp].from;
        }
        ss >> out;
        cout << out << endl;
    }
    return 0;
}

时间复杂度:O(n+m)
(2)有权最短路(Djikstra算法)
需要标记数组bool known[n],来标记到该顶点的最短路径是否确定。
初始化:把所有顶点都置成unknown,最短距离都置成\infty
s到s最短距离置为0,s设置为known。
对于和s邻接的所有顶点,最短距离记为s到它们的带权距离。
找出这些顶点中距离s最近的那个,它到s的最短距离就是它直接到s的距离。(反证:如果能从别的地方过来最短,由于权非负,它一定不是离s最近的顶点。)设这个顶点为s1,并把它设成known。
对于和s1邻接的所有顶点,最短距离记为s1到它们的带权距离。
再找出unknown顶点中s到它的距离最短的那个,设这个顶点为s2,并把它设成known。
对于和s2邻接的所有顶点,更新它们的最短距离(如果加上s2的距离比现有的距离短,就更新)。
重复以上操作直到所有的顶点都被标记成known。
对于边数很多(N=O(M^2))的图(稠密图),直接遍历所有unknown顶点,找出离s最短的顶点即可,时间复杂度O(N^2)
实现:
第一行输入m,n为顶点数和边数。
以下的n行各有三个整数,为有向边和它的权。
输入一个整数x。
输出第x个顶点到每个顶点的带权最短路径长以及最短路径,如果到不了,输出-1.

#include<fstream>
#include<iostream>
#include<vector>
#define INF 1000000
using namespace std;
int m, n;
int a;
int x;
struct edge {
    int to;
    int w;
};

struct vertex {
    int num;
    vector<edge> list;
    bool known;
    int dist;
    int from = -1;
}v[1000];

void printpath(vertex now) {
    if (now.from != -1) {
        printpath(v[now.from]);
        cout << "->";
    }
    cout << now.num;
}

edge temp;
int main() {
    ifstream fin("input.txt");
    fin >> m >> n;
    for (int i = 1; i <= n; i++) {
        fin >> a >> temp.to >> temp.w;
        v[a].list.push_back(temp);
    }
    for (int i = 1; i <= m; i++) {
        v[i].num = i;
        v[i].known = false;
        v[i].dist = INF;
    }
    fin >> x;
    v[x].dist = 0;
    int minnum;
    int mindist;
    int len;
    int num;
    while (1) {
        minnum = -1;
        mindist = INF;
        for (int i = 1; i <= m; i++) {
            if (v[i].known) continue;
            if (v[i].dist < mindist) {
                minnum = i;
                mindist = v[i].dist;
            }
        }
        if (minnum == -1) break;
        v[minnum].known = true;
        len = v[minnum].list.size();
        for (int i = 0; i < len; i++) {
            num = v[minnum].list[i].to;
            if (v[num].known) continue;
            if (v[minnum].dist + v[minnum].list[i].w < v[num].dist) {
                v[num].dist = v[minnum].dist + v[minnum].list[i].w;
                v[num].from = minnum;
            }
        }
    }
    for (int i = 1; i <= m; i++) {
        cout << i << ":" << endl << v[i].dist << endl;
        printpath(v[i]);
        cout << endl;
        cout << endl;
    }
    return 0;
}

相比上面的无权最短路:用了文件输入便于测试,将邻接表放进了vertex类里,输出路径改为了正向输出(用递归实现)。
优化:用优先队列(堆)返回距离s最近的结点,能把时间复杂度降到O((n+m)log_2n)
先补充一种数据结构:链式前向星。
相当于一个多叉树。

int top;
int head[10010];
struct Edge {
    int to;
    int w;
    int next;
}edge[200010];
void add(int u, int v, int w) {
    edge[++top].to = v;
    edge[top].w = w;
    edge[top].next = head[u];
    head[u] = top;
}

https://www.luogu.org/problem/P4779
Djikstra(堆优化版):

#include<iostream>
#include<queue>
#define INF 2147483647
using namespace std;
int n, m, s;
int head[200010], vis[100010], dist[100010];
int top;
struct Edge {
    int to;
    int next;
    int w;
}edge[200010];
struct vertex {
    int num;
    int dist;
    bool operator < (const vertex &x) const {//STL的优先队列是最大堆,要重载成最小堆
        if (x.dist < dist) return true;
        else return false;
    }
};
void add(int u, int v, int w) {
    edge[++top].to = v;
    edge[top].w = w;
    edge[top].next = head[u];
    head[u] = top;
}
vertex temp;
void Djikstra() {
    for (int i = 1; i <= n; i++) dist[i] = INF;
    priority_queue<vertex> q;
    dist[s] = 0;
    temp.num = s;
    temp.dist = 0;
    q.push(temp);
    while (!q.empty()) {
        temp = q.top();
        q.pop();
        int u = temp.num;
        if (vis[u]) continue;
        vis[u] = 1;
        for (int i = head[u]; i; i = edge[i].next) {
            int d = edge[i].to;
            if (dist[d] > dist[u] + edge[i].w) {
                dist[d] = dist[u] + edge[i].w;
                if (!vis[d]) {//只用给用到的结点编号
                    temp.num = d;
                    temp.dist = dist[d];
                    q.push(temp);
                }
            }
        }
    }
}

int main() {
    int u, v, w;
    cin >> n >> m >> s;
    for (int i = 1; i <= m; i++) {
        cin >> u >> v >> w;
        add(u, v, w);
    }
    Djikstra();
    for (int i = 1; i <= n; i++) cout << dist[i] << ' ';
    return 0;
}

(3)负权最短路(不含负环)
https://www.luogu.org/problem/P3371
对于非负权图,Djikstra最优,但是对于负权图就不行了,只能用SPFA。
SPFA:用BFS遍历每一个结点u,如果u指向的结点v原来的最短路比现在u的最短路加上u到v的距离长,则更新v的最短路,同时由于v之后结点的最短路也会改变,要将v进入队列,同时要设一个标记数组以防止重复进队。
这种算法的时间复杂度比Djikstra的优化版本大:O(MN)

#include<iostream>
#include<queue>
const long long inf = 2147483647;
const int maxn = 10005;
const int maxm = 500005;
using namespace std;
int n, m, s, top = 1;
int dist[maxn], inque[maxn], head[maxm];
struct Edge {
    int to;
    int w;
    int next;
}edge[maxm];
void add(int u, int v, int w) {
    edge[++top].to = v;
    edge[top].w = w;
    edge[top].next = head[u];
    head[u] = top;
}
void spfa() {
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        dist[i] = inf;
        inque[i] = 0;
    }
    q.push(s);
    dist[s] = 0;
    inque[s] = 1;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inque[u] = 0;
        for (int i = head[u]; i > 0; i = edge[i].next) {
            int d = edge[i].to;
            if (dist[d] > dist[u] + edge[i].w) {
                dist[d] = dist[u] + edge[i].w;
                if (inque[d] == 0) {
                    inque[d] = 1;
                    q.push(d);
                }
            }
        }
    }
}
int main() {
    cin >> n >> m >> s;
    for (int i = 1; i <= m; i++ ) {
        int u, v, w;
        cin >> u >> v >> w;
        add(u, v, w);
    }
    spfa();
    for (int i = 1; i <= n; i++) cout << dist[i] << ' ';
    return 0;
}

这种算法要保证图中不含负环(权值和为负的环),否则会无限循环。
(4)负环问题
在SPFA中,如果一个结点被访问n次,说明它所在环越加越小,故存在负环,可以设置一个cnt数组保存访问的次数。
例题:https://www.luogu.org/problem/P3385
注意每一次读数据都要清空cnt和head……
代码:https://www.luogu.org/record/22610301

5.网络流问题

https://baijiahao.baidu.com/s?id=1607147031919790970&wfr=spider&for=pc
(1)最大流问题:
对于一个有权图G,选定一个点S为起点,点T为终点,电流从S流向T,要求对于所有既不是S又不是T的顶点,流入的电流等于流出的电流,且流经
每一条边的电流都不超过该边的权,求最多有多少电流能从S流到T?

增广路:从S到T的一条路径,若选取这条路径,从S到T的流可以增加。
如果一个状态不存在增广路,则此状态确定的流为最大流。
一个朴素的想法:从S开始BFS,当找到一条到T的通路时,就把此通路所有边的权都减去这条通路上所有边的最小权,直到没有S到T的通路为止。
但是这样不对,因为可以构造反例使得这样计算的结果与选择通路的顺序有关。
EK算法:一开始给原来的图G的每条边都补上权为0的反向边,每次某一条边进行减法运算时,它的反向边同时进行加法运算,这样结果就与选择通路的顺序无关了。原因是反向边相当于可以把之前走过来的人再撵回去,从而得到更好的走法。
模板题:https://www.luogu.org/problem/P3376
本题数据规模很大,不能用vector(会超时),而且也不好访问反向边,也不能用二维数组(会超空间),可以用链式前向星。

#include<iostream>
#include<queue>
#include<algorithm>
#include<memory.h>
#define INF 1<<30
using namespace std;
int n, m, s, t;
int top = 1, head[100010];  //top表示栈顶,head[u]表示上一个起点为u的边。
bool isused[100010];  //标记某个结点是否用过
queue<int> q;
struct Edge {
    int to; //终点编号
    int w; //权
    int next; //前一个与它共起点的边
}edge[200010]; 
struct Pre {
    int v;
    int e;
}pre[100010];  //找到增广路后,某一个顶点的前一个顶点以及以它为终点的边

void add(int u, int v, int w) { //添加边
    edge[++top].to = v;
    edge[top].w = w;
    edge[top].next = head[u];
    head[u] = top;//每新加进一条边,就让它的next指向与它共起点的前一条边;对于第一条边,让它的next=0
}
int x;
int to;
bool bfs() {//广搜找增广路
    while (!q.empty()) q.pop(); //清空队列
    memset(pre, -1, sizeof(pre)); //初始化标记数组
    memset(isused, 0, sizeof(isused));
    q.push(s); //起点进队
    isused[s] = 1;  //别忘了把起点设置为isused,因为有双向边
    while (!q.empty()) { //BFS
        x = q.front();
        q.pop();//取队首元素并让其出队
        for (int i = head[x]; i > 0; i = edge[i].next){  //遍历所有以x为起点的边
            to = edge[i].to;
            if (!isused[to] && edge[i].w > 0) { //如果终点已经用过,就不要走这条路了
                pre[to].v = x; //to的前置顶点为x
                pre[to].e = i; //to的前置边为i
                if (to == t) return 1;  //终点是t,说明找到了一条增广路
                isused[to] = 1;
                q.push(to);
            }
        }
    }
    return 0;
}
int EK() {
    int res = 0;
    while (bfs()) {
        int mi = INF;
        for (int i = t; i != s; i = pre[i].v) {
            mi = min(mi, edge[pre[i].e].w);  //找到增广路上的最小边 
        }
        for (int i = t; i != s; i = pre[i].v) {
            edge[pre[i].e].w -= mi; //增广路上的正向边减去mi
            edge[pre[i].e ^ 1].w += mi;  
            //由于top从1开始,正向边为2,4,6...,反向边都是3,5,7....,所以它们仅有最后一个二进制位不同
            //用异或可以实现访问反向边
        }
        res += mi;
    }
    return res;
}
int main() {
    cin >> n >> m >> s >> t;
    int u, v, w;
    for (int i = 1; i <= m; i++) {
        cin >> u >> v >> w;
        add(u, v, w); //读入正向边
        add(v, u, 0); //读入反向边
    }
    cout << EK();
    return 0;
}

时间复杂度:O(nm^2)
(2)二分图匹配
已知A={a_1,a_2,...,a_n};B={b_1,b_2,...,b_m},对于每个a_i,都有若干b_j与之匹配,求最多能匹配多少组?
此问题可以转化为最大流问题:如果a_i能和b_j匹配,则它们之间存在价值为1的单向边,再新增两个顶点s和t,让s指向所有a,所有b指向t,求s到t的最大流即可。
当然可以用EK算法,但是求最大流有更优的Dinic算法:
先求所有顶点到s的最短距离(BFS),然后用dfs求最短距离递增的增广路,这样可以一次找到多条增广路。

#include<iostream>
#include<algorithm>
#include<queue>
#include<memory.h>
#define INF 1<<30
using namespace std;
int dep[10010], head[10010];  //dep储存s到每个结点的最短路径长
int top = 1;
int maxflow; //最大流
int n, m, s, t;
struct Edge {
    int to;
    int w;
    int next;
}edge[200010];
void add(int u, int v, int w) {
    edge[++top].to = v;
    edge[top].w = w;
    edge[top].next = head[u];
    head[u] = top;
}
bool bfs() {
    memset(dep, 0x3f, sizeof(dep)); //memset的技巧:不能直接设置大数,但可以设置0x3f,此时每个元素的值为0x3f3f3f3f
    dep[s] = 0; //s结点为0层
    queue<int> q;
    q.push(s);
    while (!q.empty()) {  //BFS求最短路径长
        int u = q.front();
        q.pop();
        for (int i = head[u]; i > 0; i = edge[i].next) {   //遍历以u为起点的边
            int d = edge[i].to;
            if (dep[d] > dep[u] + 1 && edge[i].w > 0) {  //保证u直接能到d结点比u的层数深1,注意要判断w>0防双向边
                dep[d] = dep[u] + 1;
                q.push(d);
            }
        }
    }
    if (dep[t] != 0x3f3f3f3f) return 1; //能到达t,说明能分层
    return 0; 
}

int dfs(int u,int flow) { //u:当前的结点,flow:从s到当前结点的最大流,返回值:从当前结点到t的最大流
    int rlow = 0; //余量:从当前结点到t的最大流
    if (u == t) return flow;  //已经到了t,最大流不再改变
    for (int i = head[u]; i > 0; i = edge[i].next) { //遍历以u为起点的边
        int d = edge[i].to;
        if (edge[i].w > 0 && dep[d] == dep[u] + 1) { //判断终点是否比起点的层数多1
            rlow = dfs(d, min(flow, edge[i].w)); //递归地搜索能到达t的最大流,如果到达不了,rlow=0,继续搜索别的顶点
            if (rlow > 0) { //如果能到达,沿途的正向边减rlow,负向边加rlow
                edge[i].w -= rlow;
                edge[i ^ 1].w += rlow;
                return rlow;
            }
        }
    }
    return 0;//如果搜不到rlow>0,说明按照当前层数排序已经没有增广路了,需要改变层序
}

int Dinic() {//返回最大流
    int lowflow;
    while (bfs()) { //如果能分层
        while (1) {
            lowflow = dfs(s, INF); //返回一条增广路的最大流
            if (lowflow > 0) maxflow += lowflow; 
            else break; //如果找不到增广路,break出去改变层序
        }
    }
    return maxflow;
}

int main() {
    cin >> n >> m >> s >> t;
    int u, v, w;
    for (int i = 1; i <= m; i++) {
        cin >> u >> v >> w;
        add(u, v, w);
        add(v, u, 0);
    }
    cout << Dinic();
    return 0;
}

时间复杂度:O(n^2m),适用于边数较多的图。
特别地,在二分图匹配问题中为O(n^{1/2}m)
例题:https://www.luogu.org/problem/P2756
只需要把上面的代码改一下就行,设一个不是结点的s和t,让它们分别指向外籍飞行员和被英国飞行员指向,再用res数组保存u指向d的关系(注意它的位置,必须是能走到终点的情况),输出即可。
完整代码:https://www.luogu.org/record/22582768
二分图匹配还可以用匈牙利算法,这里先欠着……
(3)最小费用最大流
https://www.luogu.org/problem/P3381
对于每条边有两个权,分别是流和流过单位流量的费用,要求在最大流的前提下,求最小费用。
把EK算法中的BFS改成SPFA即可,注意SPFA的对象是费用不是流,对于反向边,费用要变成负的。
https://www.luogu.org/record/22610301

6.最小生成树

最小生成树:对于一个无向图G,包含G的所有顶点的树中,权值和最低的树叫做G的最小生成树。
只有连通图有最小生成树,且最小生成树不包含环,即它的边数比顶点数少1。
求最小生成树:https://www.luogu.org/problem/P3366
(1)Prim
适用于稠密图。遍历所有起点u已经在树上的边(u,v),找出权值最小的边,把它加到最小生成树上。重复以上步骤直到所有顶点都在树上。

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 5010;
const int maxm = 200010;
const int INF = 1 << 30;
int n, m, u, v, w;
int top, head[maxn], dist[maxn]; //dist:到现在在树上的点的最小距离
int num, now;
int res;
bool vis[maxn];
struct Edge {
    int to;
    int next;
    int w;
}edge[maxm*2];//无向图,所有的边都是双向边
void add(int u, int v, int w) {
    edge[++top].to = v;
    edge[top].w = w;
    edge[top].next = head[u];
    head[u] = top;
}
void prim() {
    for (int i = 1; i <= n; i++) dist[i] = INF;
    now = 1;
    for (int i = head[1]; i; i = edge[i].next) {
        dist[edge[i].to] = min(dist[edge[i].to], edge[i].w);
    } //从第一个点开始设置,用min解决重边问题
    while (++num < n) { //到n结束,此时加了n-1条边
        int minn = INF;
        vis[now] = 1;
        for (int i = 1; i <= n; i++) {//找到最短的边,并把它的终点加到树上
            if (!vis[i] && minn > dist[i]) {
                minn = dist[i];
                now = i;
            }
        }
        res += minn;
        for (int i = head[now]; i; i = edge[i].next) { //更新dist
            int d = edge[i].to;
            if (dist[d] > edge[i].w && !vis[d]) dist[d] = edge[i].w;
        }
    }
}


int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        cin >> u >> v >> w;
        add(u, v, w);
        add(v, u, w);
    }
    prim();
    cout << res;
    return 0;
}

时间复杂度:O(n^2)
同理Dijkstra,堆优化后时间复杂度降到O(mlogn)
以下的模板用了pair模板类,同上面的Dijkstra模板相比要精炼许多,因为pair重载了小于运算符(见前面的某篇笔记),而且可以用make_pair构造函数。

#include<iostream>
#include<utility>
#include<queue>
using namespace std;
const int maxn = 5010;
const int maxm = 200010;
const int inf = 1 << 30;
struct Edge {
    int to;
    int w;
    int next;
}edge[maxm*2];
int top, head[maxn], dist[maxn], vis[maxn];
int n, m, u, v, w;
int cnt, res;
typedef pair<int, int> p;//因为pair类的排序是字典序,所以first是dist,second是编号
priority_queue<p, vector<p>, greater<p> > q;//最小堆
void add(int u, int v, int w) {
    edge[++top].to = v;
    edge[top].w = w;
    edge[top].next = head[u];
    head[u] = top;
}
void prim() {
    dist[1] = 0;
    for (int i = 2; i <= n; i++) dist[i] = inf;
    q.push(make_pair(0, 1));//第一个顶点进堆
    while (!q.empty() && cnt < n) {
        int minn = q.top().first;
        int u = q.top().second;
        q.pop();
        if (vis[u]) continue;
        cnt++;
        res += minn;
        vis[u] = 1;
        for (int i = head[u]; i; i = edge[i].next) {//更新顶点距离
            int d = edge[i].to;
            if (!vis[d]&&edge[i].w < dist[d]) {
                dist[d] = edge[i].w;
                q.push(make_pair(dist[d], d));
            }
        }
    }
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        cin >> u >> v >> w;
        add(u, v, w);
        add(v, u, w);
    }
    prim();
    if (cnt == n) cout << res;
    else cout<<"orz";
        return 0;
}

(2)Kruskal
适用于稀疏图。将边按照权从小到大排序,从权值最小的边开始选取,如果出现环则跳过(用并查集判断),直到边数比总点数少1。

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 5010;
const int maxm = 200010;
struct Edge {//不用链式前向星了,只用存每个边的权
    int u;
    int v;
    int w;
    bool operator < (const Edge& x) const {
        return w < x.w;
    }
}edge[maxm];
int fa[5010];
int n, m, res, u, v, cnt;
int find(int x) {//并查集模板
    if (x == fa[x]) return x;
    return fa[x] = find(fa[x]);
}
void kruskal() {
    sort(edge, edge + m); //按边权从小到大排序
    for (int i = 0; i < m; i++) {
        u = find(edge[i].u);
        v = find(edge[i].v);
        if (u == v) continue;  //如果成环(起点和终点已经连通)则跳过
        res += edge[i].w;
        fa[v] = u;//将起点和终点设为连通
        if (++cnt == n - 1) break;
    }
}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) fa[i] = i;//初始化并查集
    for (int i = 0; i < m; i++) cin >> edge[i].u >> edge[i].v >> edge[i].w;
    kruskal();
    if(cnt == n-1)cout << res;
    else cout << "orz";
    return 0;
}

7.剩下的先欠着

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260