图论模板汇总(新版)

1.Dijkstra算法

单源最短路径

P4779 【模板】单源最短路径(标准版)
给定一个 n n n 个点, m m m 条有向边的带非负权图,请你计算从 s s s 出发,到每个点的距离。
数据保证你能从 s s s 出发到任意点。

O ( m l o g m ) O(mlogm) O(mlogm)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<ll ,int>
const ll N = 1e5 + 10;
struct edge {
    int u, v;
    ll w;
};
vector<vector<edge> > e(N);
int n, m, s, vis[N];
ll dis[N];
void dijk() {
    priority_queue<P, vector<P>, greater<P> > q;
    dis[s] = 0;
    q.push({ dis[s], s });
    while (!q.empty()) {
        int u = q.top().second;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto i : e[u]) {
            if (dis[i.v] > dis[u] + i.w) {
                dis[i.v] = dis[u] + i.w;
                q.push({ dis[i.v], i.v });
            }
        }
    }
}
void solve() {
    cin >> n >> m >> s;
    for (int i = 0; i < m; ++i) {
        int u, v;
        ll w;
        cin >> u >> v >> w;
        e[u].push_back({ u, v, w });
    }
    memset(dis, 0x7f, sizeof(dis));
    dijk();
    for (int i = 1; i <= n; ++i) cout << dis[i] << " ";
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin >> _t;
    while (_t--) {
        solve();
    }
    return 0;
}

多层图Dijkstra

P4568 [JLOI2011] 飞行路线
Alice 和 Bob 现在要乘飞机旅行,他们选择了一家相对便宜的航空公司。该航空公司一共在 n n n 个城市设有业务,设这些城市分别标记为 0 0 0 n − 1 n-1 n1,一共有 m m m 种航线,每种航线连接两个城市,并且航线有一定的价格。
Alice 和 Bob 现在要从一个城市沿着航线到达另一个城市,途中可以进行转机。航空公司对他们这次旅行也推出优惠,他们可以免费在最多 k k k 种航线上搭乘飞机。那么 Alice 和 Bob 这次出行最少花费多少?

思路:各层内部正常连边,各层之间从上到下连权值为0的边。每向下跑一层,就相当于免费搭一次飞机。跑一遍从s到t+n∗k的最短路即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const ll N = 11e4 + 10;
int n, m,k,st,en;
int vis[N], dis[N];
struct edge {
    int u, v, w;
};
vector<vector<edge> > e(N);
void dijk() {
    memset(dis, 0x3f, sizeof(dis));
    priority_queue<P, vector<P>, greater<P> > q;
    dis[st] = 0;
    q.push({ dis[st], st });
    while (!q.empty()) {
        int u = q.top().second;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto i : e[u]) {
            if (dis[i.v] > dis[u] + i.w) {
                dis[i.v] = dis[u] + i.w;
                q.push({ dis[i.v], i.v });
            }
        }
    }
}
void solve() {
    cin >> n >> m >> k >> st >> en;
    for (int i = 0; i < m; ++i) {
        int x, y, c;
        cin >> x >> y >> c;
        for (int j = 0; j <= k; ++j) {
            e[x + n * j].push_back({ x + n * j, y + n * j, c });
            e[y + n * j].push_back({ y + n * j, x + n * j, c });
            if (j != k) {//向下层图连边
                e[x + n * j].push_back({ x + n * j, y + n * (j + 1), 0 });
                e[y + n * j].push_back({ y + n * j, x + n * (j + 1), 0 });
                e[en + n * j].push_back({ en + n * j, en + n * (j + 1), 0 });
            }
        }
    }
    dijk();
    cout << dis[en + n * k];
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin >> _t;
    while (_t--) {
        solve();
    }
    return 0;
}

2.SPFA算法

负权图单源最短路径

P3385 【模板】负环
给定一个 n n n 个点的有向图,请求出图中是否存在从顶点 1 1 1 出发能到达的负环。
负环的定义是:一条边权之和为负数的回路。

O ( k m ) ∼ O ( n m ) O(km)\sim O(nm) O(km)O(nm)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<ll ,int>
const ll N = 1e5 + 10;
struct edge {
    int u, v;
    ll w;
};
int n, m;
int inq[N], cnt[N];
ll dis[N];
vector<vector<edge> > e(N);
int spfa(int u) {
    memset(dis, 0x7f, sizeof(dis));
    memset(inq, 0, sizeof(inq));
    memset(cnt, 0, sizeof(cnt));
    queue<int> q;
    q.push(u);
    inq[u] = 1; 
    cnt[u] = 1;
    dis[u] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inq[u] = 0;//出队
        for (auto i : e[u]) {
            if (dis[i.v] > dis[u] + i.w) {
                dis[i.v] = dis[u] + i.w;
                cnt[i.v]++;
                if (cnt[i.v] >= n) return 1;//有负环
                if (!inq[i.v]) {
                    q.push(i.v);
                    inq[i.v] = 1;
                }
            }
        }
    }
    return 0;
}
void solve() {
    cin >> n >> m;
    e.clear();
    e.resize(N);
    for (int i = 0; i < m; ++i) {
        int u, v;
        ll w;
        cin >> u >> v >> w;
        if (w >= 0) e[v].push_back({ v, u, w });
        e[u].push_back({ u, v, w });
    }
    if (spfa(1)) cout << "YES\n";
    else cout << "NO\n";
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    //_t = 1;
    cin >> _t;
    while (_t--) {
        solve();
    }
    return 0;
}

优化:使用deque,求平均值

差分约束

P5960 【模板】差分约束算法
给出一组包含 m m m 个不等式,有 n n n 个未知数的形如:
{ x c 1 − x c 1 ′ ≤ y 1 x c 2 − x c 2 ′ ≤ y 2 ⋯ x c m − x c m ′ ≤ y m \begin{cases} x_{c_1}-x_{c'_1}\leq y_1 \\x_{c_2}-x_{c'_2} \leq y_2 \\ \cdots\\ x_{c_m} - x_{c'_m}\leq y_m\end{cases} xc1xc1y1xc2xc2y2xcmxcmym
的不等式组,求任意一组满足这个不等式组的解。

思路:差分约束系统要么无解,要么有无限组解。因为如果存在一组解,那么把每个x加上一个任意常数,方程仍然成立。
差分约束系统可以转换为最短路径问题。把约束条件 x i − x j ≤ c k x_i-x_j\leq c_k xixjck变形为 x i ≤ x j + c k x_i\leq x_j+c_k xixj+ck这与路径算法中的路径计算dis[y]=dis[x]+w很相似。把变量 x i x_i xi看作有向图的一个节点i,对每个约束条件,从节点 j 向节点 i 连一条长度为 c k c_k ck的有向边。增加一个0号点,从0点向其他所有点连一条权值为0的边。这相当于新增了一个变量 x 0 x_0 x0和n个约束条件 x i ≤ x o x_i\leq x_o xixo。dis[0]=0。
以0点为起点,用SPFA计算到其他所有点的最短路径。如果有负环,差分约束无解。如果没有负环,则有解,计算出从0点到i点的最短路径为dis[i], x i = d i s [ i ] x_i=dis[i] xi=dis[i]是差分约束系统的一组可行解。
若题目的约束条件是 x i − x j ≥ c k x_i-x_j\geq c_k xixjck变形为 x i ≥ x j + c k x_i\geq x_j+c_k xixj+ck即可。
若题目的约束条件是 x i − x j = c k x_i-x_j=c_k xixj=ck,变形为 x i ≤ x j + c k x_i\leq x_j+c_k xixj+ck x i ≥ x j + c k x_i\geq x_j+c_k xixj+ck两个差分约束系统后分别求解,若都有解,则存在解。当然,等式方程组用高斯消元更好。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 5e3 + 10;
struct edge {
    int u, v, w;
};
vector<vector<edge> >e(N);
int dist[N],inq[N],cnt[N],n,m;
bool spfa() {
    dist[0] = 0;
    queue<int> q;
    q.push(0);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inq[u] = 0;
        for (edge i : e[u]) {
            int v = i.v, w = i.w;
            if (dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                if (!inq[v]) {
                    inq[v] = 1;
                    cnt[v]++;
                    if (cnt[v] > n) return 0;
                    q.push(v);
                }
            }
        }
    }
    return 1;
}
void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) e[0].push_back({ 0, i, 0 });
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        e[v].push_back({ v, u, w });
    }
    memset(dist, 0x3f, sizeof(dist));
    if (!spfa()) cout << "NO";
    else {
        for (int i = 1; i <= n; ++i) cout << dist[i] << " ";
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

3.Floyd算法

全源最短路

O ( n 3 ) O(n^3) O(n3)

void floyd(){
    for(int k=1;k<=n;++k){
        for(int i=1;i<=n;++i){
            for(int j=1;j<=n;++j){
                dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
            }
        }
    }
}

传递闭包

N只老虎爱跳舞
某动物园里有N(1≤N≤100)只爱跳舞的老虎,被编号为1~N,平时喜欢切磋舞技并且互相不服,于是动物园给他们安排了M(1≤M≤4000)场斗舞比赛,每场比赛在两只老虎之间进行。动物园试图根据比赛结果给出每只老虎的舞技排名(假设舞技高的老虎在每场比赛中都一定会胜过相对舞技差的老虎),但是由于比赛场数有限,并不能知道有些老虎的确切舞技排名。请你给出能确定舞技精确排名的老虎的数量。确保结果不会矛盾,但是同两只老虎之间可能重复比赛(结果相同)。

思路:Floyd判断连通性

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const ll N = 1e2 + 10;
int n, m, dis[N][N];//dis=1能确定胜负
void floyd() {
    for (int k = 1; k <= n; ++k) {
        for (int i = 1; i <= n; ++i) {
            if(dis[i][k]){
                for (int j = 1; j <= n; ++j) {
                    if (dis[k][j]) dis[i][j] = 1;
                }
            }
        }
    }
}
//bitset优化
//bitset<N> dis[N];
//void floyd() {
//    for (int k = 1; k <= n; ++k) {
//        for (int i = 1; i <= n; ++i) {
//            if (dis[i][k]) dis[i] |= dis[k];
//        }
//    }
//}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) dis[i][i] = 1;
    for (int i = 1; i <= m; ++i) {
        int a, b;
        cin >> a >> b;
        dis[a][b] = 1;
    }
    floyd();
    int ans = n;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (dis[i][j] == 0 && dis[j][i] == 0) {
                ans--;
                break;
            }
        }
    }
    cout << ans;
    return 0;
}

最小环

给出一个图,问其中的由 n 个节点构成的边权和最小的环 ( n ≥ 3 ) (n\ge 3) (n3) 是多大。

记原图中 u,v 之间边的边权为 v a l ( u , v ) val\left(u,v\right) val(u,v)
我们注意到 Floyd 算法有一个性质:在最外层循环到点 k 时(尚未开始第 k 次循环),最短路数组 dis 中, d i s u , v dis_{u,v} disu,v 表示的是从 u 到 v 且仅经过编号在 [ 1 , k ) \left[1, k\right) [1,k) 区间中的点的最短路。
由最小环的定义可知其至少有三个顶点,设其中编号最大的顶点为 w,环上与 w 相邻两侧的两个点为 u,v,则在最外层循环枚举到 k=w 时,该环的长度即为 d i s u , v + v a l ( v , w ) + v a l ( w , u ) dis_{u,v}+val\left(v,w\right)+val\left(w,u\right) disu,v+val(v,w)+val(w,u)
故在循环时对于每个 k 枚举满足 i<k,j<k 的 (i,j),更新答案即可。

int n, val[N][N], dis[N][N];
int floyd() {
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) dis[i][j] = val[i][j];  //初始化最短路矩阵
    }
    int ans = INF;
    for (int k = 1; k <= n; ++k) {
        for (int i = 1; i < k; ++i) {
            for (int j = 1; j < i; ++j) ans = min(ans, dis[i][j] + val[i][k] + val[k][j]);  //更新答案
        }
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j)dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);  //floyd 更新最短路矩阵
        }
    }
    return ans;
}

4.Johnson算法

能解决负环
P5905 【模板】Johnson 全源最短路
给定一个包含 n n n 个结点和 m m m 条带权边的有向图,求所有点对间的最短路径长度,一条路径的长度定义为这条路径上所有边的权值和。
注意:

  1. 边权可能为负,且图中可能存在重边和自环;
  2. 部分数据卡 n n n 轮 SPFA 算法。

思路:新建一个0节点。从这个点向其他所有点连一条边权为 0 的边。接下来用 SPFA 算法求出从 0 号点到其他所有点的最短路,记为 h i h_i hi 。假如存在一条从 u 点到 v 点,边权为 w 的边,则我们将该边的边权重新设置为 w + h u − h v w+h_u −h_v w+huhv 。接下来以每个点为起点,跑 n 轮 Dijkstra 算法即可求出任意两点间的最短路了。

O ( n m l o g m ) O(nmlogm) O(nmlogm)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 3e3 + 10;
struct edge {
    int u, v, w;
};
vector<vector<edge> >e(N);
int n, m, inq[N], dis[N], dist[N][N], vis[N], cnt[N];
bool spfa() {
    queue<int> q;
    q.push(0);
    dis[0] = 0;
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inq[u] = 0;
        for (auto i : e[u]) {
            if (dis[u] + i.w < dis[i.v]) {
                dis[i.v] = dis[u] + i.w;
                cnt[i.v]++;
                if (cnt[i.v] > n) return 0;
                if (!inq[i.v]) {
                    inq[i.v] = 1;
                    q.push(i.v);
                }
            }
        }
    }
    return 1;
}
void dijk(int x) {
    priority_queue<P, vector<P>, greater<P> > q;
    dist[x][x] = 0;
    q.push({ dist[x][x],x });
    while (!q.empty()) {
        int u = q.top().second;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto i : e[u]) {
            if (dist[x][u] + i.w < dist[x][i.v]) {
                dist[x][i.v] = dist[x][u] + i.w;
                q.push({ dist[x][i.v],i.v });
            }
        }
    }
}
void solve() {
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        e[u].push_back({ u,v,w });
    }
    for (int i = 1; i <= n; ++i) e[0].push_back({ 0,i,0 });
    memset(dis, 0x3f, sizeof(dis));
    if (!spfa()) {
        cout << -1;
        return;
    }
    for (int i = 1; i <= n; ++i) {
        for (auto& j : e[i]) {
            j.w += dis[i] - dis[j.v];
        }
    }
    memset(dist, 0x3f, sizeof(dist));
    for (int i = 1; i <= n; ++i) {
        memset(vis, 0, sizeof(vis));
        dijk(i);
    }
    for (int i = 1; i <= n; ++i) {
        ll ans = 0;
        for (int j = 1; j <= n; ++j) {
            if (dist[i][j] == 0x3f3f3f3f) dist[i][j] = 1e9;
            else dist[i][j] -= dis[i] - dis[j];
            ans += 1ll * j * dist[i][j];
        }
        cout << ans << '\n';
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

5.k短路径

P2901 [USACO08MAR] Cow Jogging G
贝西终于尝到了懒惰的后果,决定每周从谷仓到池塘慢跑几次来健身。当然,她不想跑得太累,所以她只打算从谷仓慢跑下山到池塘,然后悠闲地散步回谷仓。
同时,贝西不想跑得太远,所以她只想沿着通向池塘的最短路径跑步。一共有 M M M 条道路,其中每一条都连接了两个牧场。这些牧场从 1 1 1 N N N 编号,如果 X > Y X>Y X>Y,则说明牧场 X X X 的地势高于牧场 Y Y Y,即下坡的道路是从 X X X 通向 Y Y Y 的, N N N 为贝西所在的牛棚(最高点), 1 1 1 为池塘(最低点)。
然而,一周之后,贝西开始对单调的路线感到厌烦,她希望可以跑不同的路线。比如说,她希望能有 K K K 种不同的路线。同时,为了避免跑得太累,她希望这 K K K 条路线是从牛棚到池塘的路线中最短的 K K K 条。如果两条路线包含的道路组成的序列不同,则这两条路线被认为是不同的。
请帮助贝西算算她的训练强度,即将牧场网络里最短的 K K K 条路径的长度分别算出来。你将会被提供一份牧场间路线的列表,每条道路用 ( X i , Y i , D i ) (X_i, Y_i, D_i) (Xi,Yi,Di) 表示,意为从 X i X_i Xi Y i Y_i Yi 有一条长度为 D i D_i Di 的下坡道路。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e3 + 10;
struct edge {
    int u, v, w;
};
struct node {
    int id;
    ll g, h;
    bool operator<(const node a)const {
        return g + h > a.g + a.h;
    }
};
vector<vector<edge> >e(N);
vector<vector<edge> >e2(N);
int n, m, k;
ll dis[N], cnt[N],vis[N],ans[110];
void dijk(int x) {
    dis[x] = 0;
    priority_queue<P, vector<P>, greater<P> >q;
    q.push({ dis[x], x });
    while (!q.empty()) {
        int u = q.top().second;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto i : e2[u]) {
            if (dis[i.v] > dis[u] + i.w) {
                dis[i.v] = dis[u] + i.w;
                q.push({ dis[i.v],i.v });
            }
        }
    }
}
void astar(int s, int t, int k) {
    priority_queue<node> q;
    q.push({ s, 0, dis[s] });
    while (!q.empty()) {
        node u = q.top();
        q.pop();
        cnt[u.id]++;
        if (u.id == t) {
            ans[cnt[u.id]] = u.g;
            if (cnt[u.id] == k) return;
        }
        for (auto i : e[u.id]) {
            q.push({ i.v, u.g+i.w, dis[i.v] });
        }
    }
}
void solve() {
    cin >> n >> m >> k;
    for (int i = 1; i <= m; ++i) {
        int x, y, z;
        cin >> x >> y >> z;
        e[x].push_back({ x, y, z });
        e2[y].push_back({ y, x, z });
     }
    memset(dis, 0x7f, sizeof(dis));
    for (int i = 1; i <= k; ++i) ans[i] = -1;
    dijk(1);
    astar(n, 1, k);
    for (int i = 1; i <= k; ++i) cout << ans[i] << '\n';
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

6.Kruskal算法

P3366 【模板】最小生成树
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 orz

O ( m l o g m ) O(mlogm) O(mlogm)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
const ll N = 5e3 + 10;
const ll M = 2e5 + 10;
int fa[N], n, m;
struct edge {
    int u, v, w;
    bool operator<(const edge a)const {
        return w < a.w;
    }
}e[M];
int find(int x) {
    if (x == fa[x]) return x;
    return fa[x] = find(fa[x]);
}
int main() {
    cin >> n >> m;
    ll ans = 0, cnt = 0;
    for (int i = 1; i <= n; ++i) fa[i] = i;
    for (int i = 1; i <= m; ++i) {
        int x, y, z;
        cin >> x >> y >> z;
        e[i] = { x, y, z };
    }
    sort(e + 1, e + m + 1);
    for (int i = 1; i <= m; ++i) {
        int u = e[i].u, v = e[i].v, w = e[i].w;
        u = find(u), v = find(v);
        if (u != v) { //并查集
            fa[u] = v;
            ans += e[i].w;
            cnt++;
        }
        if (cnt == n - 1) break;
    }
    if (cnt == n - 1) cout << ans;
    else cout << "orz";
    return 0;
}

7.Prim算法

P3366 【模板】最小生成树
O ( m l o g n ) O(mlogn) O(mlogn)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
const ll N = 5e3 + 10;
int n, m, vis[N];
ll ans;
struct edge {
    int u, v, w;
};
struct node {
    int id, dis;
    bool operator<(const node a)const {
        return dis > a.dis;
    }
};
vector<vector<edge> > e(N);
bool  prim() {
    int cnt = 0;
    priority_queue<node> q;
    q.push({ 1, 0 });
    while (!q.empty()) {
        node u = q.top();
        q.pop();
        if (vis[u.id]) continue;
        vis[u.id] = 1;
        ans += u.dis;
        cnt++;
        for (auto i : e[u.id]) {
            if (vis[i.v]) continue;
            q.push({ i.v, i.w });
        }
        if (cnt == n) return 1;
    }
    return 0;
}
void solve() {
    cin >> n >> m;
    ans = 0;
    for (int i = 0; i < m; ++i) {
        int x, y, z;
        cin >> x >> y >> z;
        e[x].push_back({ x, y, z });
        e[y].push_back({ y, x, z });
    }
    if (prim()) cout << ans;
    else cout << "orz";
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

8.Kruskal重构树

用来处理最小生成树的最大边权问题
重构树的过程:将所有边按边权从小到大排序。每次最小的一条边,如果条边相连的两个点在同一个集合中,那么就跳过,否则就将这两个点的祖先都连到一个虚点上去,让这个虚点的点权等于这条边的边权。
重构树性质:原本最小生成树上的点在重构树里都是叶节点,其余结点都是代表原图中的边。二叉堆,即一个子树的根节点权值必定大于他内部所有结点的权值。任意两点之间路径的最大边权就是他们的LCA的点权。

P2245 星际导航
sideman \text{sideman} sideman 做好了回到 Gliese \text{Gliese} Gliese 星球的硬件准备,但是 sideman \text{sideman} sideman 的导航系统还没有完全设计好。为了方便起见,我们可以认为宇宙是一张有 N N N 个顶点和 M M M 条边的带权无向图,顶点表示各个星系,两个星系之间有边就表示两个星系之间可以直航,而边权则是航行的危险程度。
sideman \text{sideman} sideman 现在想把危险程度降到最小,具体地来说,就是对于若干个询问 ( A , B ) (A, B) (A,B) sideman \text{sideman} sideman 想知道从顶点 A A A 航行到顶点 B B B 所经过的最危险的边的危险程度值最小可能是多少。作为 sideman \text{sideman} sideman 的同学,你们要帮助 sideman \text{sideman} sideman 返回家园,兼享受安全美妙的宇宙航行。所以这个任务就交给你了。

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<ll,ll>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 3e5 + 10;
vector<vector<int> >p(N);
struct edge {
    int u, v, w;
    bool operator<(const edge a)const {
        return w < a.w;
    }
}e[N];
int fa[N], f[N][20], dep[N], idx, val[N];
int find(int x) {
    if (x == fa[x]) return x;
    else return fa[x] = find(fa[x]);
}
void dfs(int x, int father) {
    dep[x] = dep[father] + 1;
    f[x][0] = father;
    for (int i = 1; i < 20; ++i) f[x][i] = f[f[x][i - 1]][i - 1];
    for (int i : p[x]) {
        dfs(i, x);
    }
}
int lca(int x, int y) {
    if (dep[x] < dep[y])swap(x, y);
    for (int i = 19; i >= 0; --i) {
        if (dep[f[x][i]] >= dep[y]) x = f[x][i];
    }
    if (x == y) return x;
    for (int i = 19; i >= 0; --i) {
        if (f[x][i] != f[y][i]) {
            x = f[x][i];
            y = f[y][i];
        }
    }
    return f[x][0];
}
inline void solve() {
    int n, m, q;
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        e[i] = { u,v,w };
    }
    sort(e + 1, e + m + 1);
    int cnt = 0;
    idx = n;
    for (int i = 1; i <= n * 2; ++i) fa[i] = i;
    for (int i = 1; i <= m; ++i) {
        int u = find(e[i].u), v = find(e[i].v);
        if (u != v) {
            fa[u] = fa[v] = ++idx;
            p[idx].push_back(u);
            p[idx].push_back(v);
            val[idx] = e[i].w;
            if (++cnt == n - 1) break;
        }
    }
    for (int i = 1; i <= n; ++i) {
        if (!dep[find(i)]) dfs(find(i), 0);
    }
    cin >> q;
    for (int i = 1; i <= q; ++i) {
        int u, v;
        cin >> u >> v;
        if (find(u) != find(v)) cout << "impossible\n";
        else {
            int d = lca(u, v);
            cout << val[d] << '\n';
        }
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t = 1;
    //_t = read();
    while (_t--) {
        solve();
    }
    return 0;
}

9.严格次小生成树

P4180 [BJWC2010] 严格次小生成树
小 C 最近学了很多最小生成树的算法,Prim 算法、Kruskal 算法、消圈算法等等。正当小 C 洋洋得意之时,小 P 又来泼小 C 冷水了。小 P 说,让小 C 求出一个无向图的次小生成树,而且这个次小生成树还得是严格次小的,也就是说:如果最小生成树选择的边集是 E M E_M EM,严格次小生成树选择的边集是 E S E_S ES,那么需要满足:( v a l u e ( e ) value(e) value(e) 表示边 e e e 的权值) ∑ e ∈ E M v a l u e ( e ) < ∑ e ∈ E S v a l u e ( e ) \sum_{e \in E_M}value(e)<\sum_{e \in E_S}value(e) eEMvalue(e)<eESvalue(e)
这下小 C 蒙了,他找到了你,希望你帮他解决这个问题。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 3e5 + 10;
struct edge {
    int u, v, w;
    bool operator<(const edge a)const {
        return w < a.w;
    }
}e[N];
int n, m, deep[N], f[N][20], dist1[N][20], dist2[N][20],fa[N],intree[N],d[N],cnt=0,num=0,cnta[N],cntb[N];
ll sum = 0,ans=INF;
vector<vector<edge> >p(N); 
inline int read() {
    char c = getchar(); int x = 0;
    while (c < '0' || c > '9') { c = getchar(); }
    while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
    return x;
}
inline void init() {
    for (register int i = 1; i <= n; ++i) fa[i] = i;
}
inline int find(int x) {
    if (x == fa[x]) return x;
    return fa[x] = find(fa[x]);
}
inline void merge(int a, int b) {
    a = find(a);
    b = find(b);
    if (cnta[a] < cntb[b]) fa[a] = b;
    else {
        if (cnta[a] == cntb[b]) cnta[a]++;
        fa[b] = a;
    }
}
inline bool query(int a, int b) {
    return find(a) == find(b);
}
void dfs(int x) {
    for (edge i : p[x]) {
        int v = i.v, w = i.w;
        if (v == f[x][0]) continue;
        deep[v] = deep[x] + 1;
        f[v][0] = x;
        dist1[v][0] = w;
        for (register int j = 1; (1 << j) <= deep[v]; ++j) {
            f[v][j] = f[f[v][j - 1]][j - 1];
            d[0] = dist1[v][j - 1], d[1] = dist1[f[v][j - 1]][j - 1], d[2] = dist2[v][j - 1], d[3] = dist2[f[v][j - 1]][j - 1];
            for (register int k = 0; k < 4; ++k) {
                if (d[k] > dist1[v][j]) dist2[v][j] = dist1[v][j],dist1[v][j] = d[k] ;
                else if (d[k] != dist1[v][j] && d[k] > dist2[v][j]) dist2[v][j] = d[k];
            }
        }
        dfs(v);
    }
}
int lca(int x, int y,int w) {
    if (deep[x] < deep[y]) swap(x, y);
    for (register int i = 19; i >= 0; --i) {
        if (deep[f[x][i]] >= deep[y]) {
            d[++num] = dist1[x][i];
            d[++num] = dist2[x][i];
            x = f[x][i];
        }
    }
    if (x != y) {
        for (register int i = 19; i >= 0; --i) {
            if (f[x][i] != f[y][i]) {
                d[++num] = dist1[x][i];
                d[++num] = dist1[y][i];
                d[++num] = dist2[x][i];
                d[++num] = dist2[y][i];
                x = f[x][i];
                y = f[y][i];
            }
        }
        d[++num] = dist1[x][0];
        d[++num] = dist1[y][0];
    }
    int d1 = 0, d2 = 0;
    for (register int i = 1; i <= num; ++i) {
        if (d[i] > d1) d2 = d1,d1 = d[i];
        else if (d[i] != d1 && d[i] > d2) d2 = d[i];
    }
    if (w == d1) return w - d2;
    else return w - d1;
}
void solve() {
    n=read(),m=read();
    for (register int i = 1; i <= m; ++i) {
        int u, v, w;
        u = read(), v = read(), w = read();
        e[i] = { u, v, w };
    }
    sort(e + 1, e + m + 1);
    init();
    for (register int i = 1; i <= m; ++i) {
        int u = e[i].u, v = e[i].v,w=e[i].w;
        if (u == v) continue;
        if (!query(u,v)) {
            merge(u, v);
            cnt++;
            sum += w;
            p[u].push_back({ u, v, w });
            p[v].push_back({ v, u, w });
            intree[i] = 1;
        }
        if (cnt == n - 1) break;
    }
    deep[1] = 1;
    dfs(1);
    for (register int i = 1; i <= m; ++i) {
        if (intree[i]) continue;
        int u = e[i].u, v = e[i].v,w=e[i].w;
        if (u == v) continue;
        num = 0;
        ans = min(ans, sum + lca(u, v, w));
    }
    cout << ans;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

10.拓扑排序

O ( n + m ) O(n+m) O(n+m)

void topo() {
    queue<int> q;
    for (int i = 1; i <= n; ++i) {
        if (in[i] == 0) q.push(i);
    }
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int i : e[u]) {
            in[i]--;
            if (in[i] == 0) q.push(i);
        }
    }
}

11.欧拉路

欧拉路:从图中某个点出发,遍历整个图,图中每条迈通过具不地过。
欧拉回路是起点和终点相同的欧拉路。

首先,图应该是连通图。编程时用DFS或并查集判断连通性。
其次,判断图是否存在欧拉路或欧拉回路。
(1)无向连通图的判断条件。如果图中的点全都是偶点,则存在欧拉回路,任意点都可以作为起点和终点。如果只有两个奇点,则存在欧拉路,其中一个奇点是起点,另一个是终点。不可能出现有奇数个奇点的无向图。
(2)有向连通图的判断条件。把一个点的出度记为1,入度记为-1,这个点所有出度和入度相加,就是它的度数。一个有向图存在欧拉回路的条件是当且仅当该图所有点的度数为0。存在欧拉路径的条件是只有一个度数为1的点,一个度数为-1的点,其他所有点的度数为0,其中度数为1的是起点,度数为-1的是终点。

12.基环树dp

P1453 城市环路
整个城市可以看做一个 n n n 个点, n n n 条边的单圈图(保证图连通),唯一的环便是绕城的环路。保证环上任意两点有且只有 2 2 2 条简单路径互通。图中的其它部分皆隶属城市郊区。
现在,有一位名叫 Jim 的同学想在 B 市开店,但是任意一条边的 2 2 2 个点不能同时开店,每个点都有一定的人流量,第 i i i 个点的人流量是 p i p_i pi,在该点开店的利润就等于 p i × k p_i×k pi×k,其中 k k k 是一个常数。
Jim 想尽量多的赚取利润,请问他应该在哪些地方开店?

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const ll N = 1e5 + 10;
vector<vector<int> >e(N);
ll n, p[N],in[N],vis[N],dp[N][2];
double k;
void dfs(int a,int b,int x) {
    vis[x]=1;
    dp[x][0] = dp[x][1] = 0;
    for (int i : e[x]) {
        if (vis[i]||x == b && i == a) continue;
        dfs(a, b, i);
        dp[x][1] += dp[i][0];
        dp[x][0] += max(dp[i][1], dp[i][0]);
    }
    dp[x][1] += p[x];
}
void topo() {
    queue<int> q;
    for (int i = 0; i < n; ++i) {
        if (in[i] == 1) q.push(i);
    }
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int i : e[u]) {
            in[i]--;
            if (in[i] == 1) q.push(i);
        }
    }
}
void solve() {
    cin >> n;
    for (int i = 0; i < n; ++i) cin >> p[i];
    for (int i = 0; i < n; ++i) {
        int a, b;
        cin >> a >> b;
        e[a].push_back(b);
        e[b].push_back(a);
        in[a]++;
        in[b]++;
    }
    cin >> k;
    topo();
    for (int i = 0; i < n; ++i) {
        if (in[i] == 2) {
            for (int j : e[i]) {
                if (in[j] == 2) {
                    dfs(j, i, i);
                    ll ans = dp[i][0];
                    memset(vis, 0, sizeof(vis));
                    dfs(i, j, j);
                    cout << setiosflags(ios::fixed) << setprecision(1) << max(ans, dp[j][0]) * k;
                    return;
                }
            }
        }
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

13.割点

P3388 【模板】割点(割顶)
给出一个 n n n 个点, m m m 条边的无向图,求图的割点。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const ll N = 2e4 + 10;
vector<vector<int> >e(N);
int n, m, num[N], low[N], cnt = 0,dfn = 0,cut[N];
void tarjan(int x,int fa) {
    num[x] = low[x] = ++dfn;
    int child = 0;
    for (int i : e[x]) {
        if (num[i] == 0) {
            child++;
            tarjan(i, x);
            low[x] = min(low[x], low[i]);
            if (num[x] <= low[i] && x != fa) cut[x] = 1;//num[x]<low[i]割边
        }
        else if (i != fa) low[x] = min(low[x], num[i]);
    }
    if (x == fa && child >= 2) cut[x] = 1;
}
void solve() {
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        int x, y;
        cin >> x >> y;
        e[x].push_back(y);
        e[y].push_back(x);
    }
    for (int i = 1; i <= n; ++i) {
        if (!num[i]) tarjan(i,i);
    }
    for (int i = 1; i <= n; ++i) cnt += cut[i];
    cout << cnt << '\n';
    for (int i = 1; i <= n; ++i) {
        if (cut[i]) cout << i << " ";
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

14.点双连通分量

P8435 【模板】点双连通分量
对于一个 n n n 个节点 m m m 条无向边的图,请输出其点双连通分量的个数,并且输出每个点双连通分量。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 5e5 + 10;
vector<vector<int> >e(N);
vector<vector<int> >vdcc(N);
int num[N], low[N], dfn, tot;
stack<int> st;
void tarjan(int x, int fa) {
    st.push(x);
    num[x] = low[x] = ++dfn;
    int child = 0;
    for (int i : e[x]) {
        if (!num[i]) {
            child++;
            tarjan(i, x);
            low[x] = min(low[x], low[i]);
            if (num[x] <= low[i]) {
                tot++;
                int u = -1;
                while (u != i) {
                    u = st.top();
                    st.pop();
                    vdcc[tot].push_back(u);
                }
                vdcc[tot].push_back(x);
            }
        }
        else if (i != fa) low[x] = min(low[x], num[i]);
    }
    if (x == fa && child == 0)  vdcc[++tot].push_back(x);
}
void solve() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    for (int i = 1; i <= n; ++i) {
        if (!num[i]) tarjan(i, i);
    }
    cout << tot << '\n';
    for (int i = 1; i <= tot; ++i) {
        cout << vdcc[i].size() << " ";
        for (int j : vdcc[i]) cout << j << " ";
        cout << '\n';
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

15.边双连通分量

P8436 【模板】边双连通分量
对于一个 n n n 个节点 m m m 条无向边的图,请输出其边双连通分量的个数,并且输出每个边双连通分量。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 4e6 + 10;
vector<vector<int> >edcc(N);
int h[N], nx[N], to[N], cnt = 1;
int num[N], low[N], dfn, tot;
stack<int> st;
void add(int x, int y) {
    nx[++cnt] = h[x], h[x] = cnt, to[cnt] = y;
}
void tarjan(int x, int edge) {
    st.push(x);
    num[x] = low[x] = ++dfn;
    for (int i = h[x]; i; i = nx[i]) {
        int v = to[i];
        if (!num[v]) {
            tarjan(v, i);
            low[x] = min(low[x], low[v]);
        }
        else  if ((i ^ 1) != edge) low[x] = min(low[x], num[v]);
    }
    if (num[x] == low[x]) {
        ++tot;
        int u = -1;
        while (u != x) {
            u = st.top();
            st.pop();
            edcc[tot].push_back(u);
        }
    }
}
void solve() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        if (u > v) swap(u, v);
        add(u, v), add(v, u);
    }
    for (int i = 1; i <= n; ++i) {
        if (!num[i]) tarjan(i, -1);
    }
    cout << tot << '\n';
    for (int i = 1; i <= tot; ++i) {
        cout << edcc[i].size() << " ";
        for (int j : edcc[i]) cout << j << " ";
        cout << '\n';
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

16.缩点(tarjan算法)

P3387 【模板】缩点
给定一个 n n n 个点 m m m 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

O ( n + m ) O(n+m) O(n+m)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const ll N = 1e4 + 10;
vector<vector<int> >e(N);
vector<vector<int> >p(N);
stack<int> st;
int a[N],n,m,num[N],low[N],sccno[N],cnt=0,val[N],in[N],dp[N],vis[N],dfn=0;
void tarjan(int x) {
    st.push(x);
    num[x] = low[x] = ++dfn;
    for (int i : e[x]) {
        if (num[i] == 0) {
            tarjan(i);
            low[x] = min(low[x], low[i]);
        }
        else if (sccno[i] == 0) low[x] = min(low[x], num[i]);
    }
    if (low[x] == num[x]) {
        cnt++;
        int u = -1;
        while (u != x) {
            u = st.top();
            st.pop();
            sccno[u] = cnt;
        }
    }
}
void topo() {
    queue<int> q;
    for (int i = 1; i <= cnt; ++i) {
        if (in[i] == 0)q.push(i);
        dp[i] = val[i];
    }
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int i : p[u]) {
            dp[i] = max(dp[i], dp[u] + val[i]);
            in[i]--;
            if (in[i] == 0) q.push(i);
        }
    }
}
void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    for (int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v);
    }
    for (int i = 1; i <= n; ++i) {
        if (num[i] == 0) tarjan(i);
    }
    for (int i = 1; i <= n; ++i) {
        for (int j : e[i]) {
            if (sccno[i] != sccno[j]) {
                p[sccno[i]].push_back(sccno[j]);
                in[sccno[j]]++;
            }
        }
    }
    topo();
    int ans = 0;
    for (int i = 1; i <= cnt; ++i) {
        ans = max(ans, dp[i]);
    }
    cout << ans;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

17.Kosaraju算法

先记录原图 G 和反图 rG
对原图所有的点 DFS 一遍,标记点的先后顺序
根据点的逆序在反图 DFS 一遍,能搜到的点就是在同一个强连通分量。

hdu1269 迷宫城堡
判断整个图是否为强连通分量

O ( n + m ) O(n+m) O(n+m)

#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
vector<int> G[N], rG[N];
vector<int> S; // 存第一次dfs1()的结果,即标记点的先后顺序,优先级小的点先进
int vis[N]; // vis[i]标记第一次dfs1()点i是否访问过
int sccno[N]; // sccno[i]标记点i属于第几个强连通分量,同时记录dfs2()过程中点i是否访问过
int cnt; //cnt表示强连通分量的个数
void dfs1(int u) {
    if (vis[u])  return;
    vis[u] = 1;
    for (int i = 0; i < G[u].size(); i++)
        dfs1(G[u][i]);
    S.push_back(u); //记录点的先后顺序,按照拓扑排序,优先级大的放在S的后面
}
void dfs2(int u) {
    if (sccno[u])    return;
    sccno[u] = cnt;
    for (int i = 0; i < rG[u].size(); i++)
        dfs2(rG[u][i]);
}
void Kosaraju(int n) {
    cnt = 0;
    S.clear();
    memset(vis, 0, sizeof(vis));
    memset(sccno, 0, sizeof(sccno));
    for (int i = 1; i <= n; i++) //搜索所有点
        dfs1(i);
    for (int i = n - 1; i >= 0; i--) {
        if (!sccno[S[i]]) {
            cnt++;
            dfs2(S[i]);
        }
    }
}
int main() {
    int n, m, u, v;
    //坑点,这里 n 或者 m 都不能为 0 
    while (cin >> n >> m && (n || m)) {
        for (int i = 0; i < n; i++) {
            G[i].clear();
            rG[i].clear();
        }
        for (int i = 0; i < m; i++) {
            cin >> u >> v;
            G[u].push_back(v); // 原图
            rG[v].push_back(u); // 反图
        }
        Kosaraju(n);
        cnt == 1 ? cout << "Yes" << endl : cout << "No" << endl;
    }
    return 0;
}

18.2-SAT问题

P4782 【模板】2-SAT 问题
n n n 个布尔变量 x 1 x_1 x1 ∼ \sim x n x_n xn,另有 m m m 个需要满足的条件,每个条件的形式都是 「 x i x_i xitrue / false x j x_j xjtrue / false」。比如 「 x 1 x_1 x1 为真或 x 3 x_3 x3 为假」、「 x 7 x_7 x7 为假或 x 2 x_2 x2 为假」。
2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const int N = 2e6 + 10;
vector<vector<int> >e(N);
int n, m, num[N], low[N], sscno[N], dfn = 0, cnt = 0;
stack<int> st;
void tarjan(int x) {
    st.push(x);
    num[x] = low[x] = dfn++;
    for (int i : e[x]) {
        if (!num[i]) {
            tarjan(i);
            low[x] = min(low[x], low[i]);
        }
        else if (!sscno[i]) {
            low[x] = min(low[x], num[i]);
        }
    }
    if (low[x] == num[x]) {
        ++cnt;
        int u = -1;
        while (u != x) {
            u = st.top();
            st.pop();
            sscno[u] = cnt;
        }
    }
}
void solve() {
    cin >> n >> m;
    for (int i = 1; i <= m; ++i) {
        int a, b, c, d;
        cin >> a >> b >> c >> d;
        e[a + (1 - b) * n].push_back(c + d * n);//一个为0,另一个必为1
        e[c + (1 - d) * n].push_back(a + b * n);
    }
    for (int i = 1; i <= 2 * n; ++i) {
        if (!num[i])tarjan(i);
    }
    for (int i = 1; i <= n; ++i) {
        if (sscno[i] == sscno[i + n]) {//在同一个SCC中
            cout << "IMPOSSIBLE";
            return;
        }
    }
    cout << "POSSIBLE\n";
    for (int i = 1; i <= n; ++i) {
        cout << (sscno[i] > sscno[i + n]) << " ";
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

19.EK算法

P3376 【模板】网络最大流
如题,给出一个网络图,以及其源点和汇点,求出其网络最大流。

思路:Ford-Fulkerson方法
(1)初始时,所有边上的流量为0。
(2)找到一条从s到t的路径,按最大流的3个性质,得到这条路径上的最大流,更新每条边的残留容量。残留容量在后续步骤中继续使用。
(3)重复步骤(2),直到找不到路径。

O ( n m 2 ) O(nm^2) O(nm2)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e4 + 10;
struct edge {
    int u, v, n;
    ll f;
}e[N];
ll  flow[N];
int n, m, s, t, cnt = 1, head[N], pre[N];
void add(int u, int v, ll f) {
    cnt++;
    e[cnt] = { u, v, f, head[u] };
    head[u] = cnt;
}
bool bfs() {
    memset(pre, 0, sizeof(pre));
    flow[s] = INF;
    queue<int> q;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        if (u == t) return 1;
        for (int i = head[u]; i; i = e[i].n) {
            int v = e[i].v;
            ll f = e[i].f;
            if (f > 0 && !pre[v]) {
                flow[v] = min(flow[u], f);
                pre[v] = i;
                q.push(v);
            }
        }
    }
    return 0;
}
ll MaxFlow() {
    ll ans = 0;
    while (1) {
        if (!bfs()) break;//找到一条路径
        ans += flow[t];
        int cur = t;
        while (cur != s) {//沿路径回溯到起点
            e[pre[cur]].f -= flow[t];//正向边减
            e[pre[cur] ^ 1].f += flow[t];//反向边加
            cur = e[pre[cur]].u;
        }
    }
    return ans;
}
void solve() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; ++i) {
        int u, v;
        ll f;
        cin >> u >> v >> f;
        add(u, v, f);
        add(v, u, 0);
    }
    cout << MaxFlow();
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

20.Dinic算法

P3376 【模板】网络最大流
思路:对于图中的每个点,如果它有多个分支,对所有分支都进行一次增广路径搜索,而不是从源点重新开始增广。Dinic算法是BFS和DFS的结合。
(1)BFS分层。从源点s开始,用BFS求出节点的层次,构造分层图。分层图的作用是限制DFS的搜索范围,在分层图中的任意路径都是边数最少的最短路径。
(2)DFS增广。一次DFS,多次增广。在分层图上,对每个点用DFS搜索每个分支。由于是在分层图上DFS的,DFS的路径只能一层层往后走到汇点,而不会兜圈子绕路。当DFS处理到一个点v时,沿着它的一个分支找到一条从v到达汇点t的路径,然后在回溯到v的过程中,根据这条路径上用掉的流量的大小,更新路径上边的容量,这样在后面继续做新的DFS时,可用的容量便减少了。最后,在更新容量后的图中无法再找到一条到达汇点的路径,算法结束

O ( n 2 m ) O(n^2m) O(n2m)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e4 + 10;
struct edge {
    int u, v, n;
    ll f;
}e[N];
int n, m, s, t, cnt = 1, head[N],deep[N],now[N];
void add(int u, int v, ll f) {
    cnt++;
    e[cnt] = { u, v, head[u], f };
    head[u] = cnt;
}
bool bfs() {
    memset(deep, 0, sizeof(deep));
    memcpy(now, head, sizeof(head));
    deep[s] = 1;
    queue<int> q;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (int i = head[u]; i; i = e[i].n) {
            int v = e[i].v;
            ll f = e[i].f;
            if (f > 0 && !deep[v]) {
                deep[v] = deep[u] + 1;
                if (v == t) return 1;
                q.push(v);
            }
        }
    }
    return 0;
}
ll dfs(int u, ll sum) {
    if (u == t) return sum;
    ll flow = 0;
    for (int i = now[u]; i&&sum; i = e[i].n) {
        now[u] = i;
        int v = e[i].v;
        ll f = e[i].f;
        if (f > 0 && deep[v] == deep[u] + 1) {
            ll k = dfs(v, min(sum, f));
            if (k == 0) deep[v] = 0;
            e[i].f -= k;
            e[i ^ 1].f += k;
            flow += k;
            sum -= k;
        }
    }
    return flow;
}
void solve() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; ++i) {
        int u, v;
        ll f;
        cin >> u >> v >> f;
        add(u, v, f);
        add(v, u, 0);
    }
    ll ans = 0;
    while (bfs()) ans += dfs(s, INF);
    cout << ans;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

21.ISAP算法

P3376 【模板】网络最大流
思路:ISAP算法只做一次BFS分层,然后在这个分层图上多次寻找增广路径。但是,寻找一次增广路径后,会生成新的残留网络,需要在这个残留网络上更新层次。

O ( n 2 m ) O(n^2m) O(n2m)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e4 + 10;
struct edge {
    int u, v, n;
    ll f;
}e[N];
int n, m, s, t, cnt = 1, head[N], pre[N],now[N],gap[N],deep[N];
void add(int u, int v, ll f) {
    cnt++;
    e[cnt] = { u, v, head[u], f };
    head[u] = cnt;
}
void bfs() {
    memcpy(now, head, sizeof(head));
    deep[t] = 1;
    queue<int> q;
    q.push(t);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        gap[deep[u]]++;
        for (int i = head[u]; i; i = e[i].n) {
            int v = e[i].v;
            ll f = e[i^1].f;
            if (!deep[v] && f) {
                deep[v] = deep[u] + 1;
                q.push(v);
            }
        }
    }
}
ll augement() {
    int cur = t;
    ll flow = INF;
    while (cur != s) {
        flow = min(flow, e[pre[cur]].f);
        cur = e[pre[cur]].u;
    }
    cur = t;
    while (cur != s) {
        e[pre[cur]].f -= flow;
        e[pre[cur] ^ 1].f += flow;
        cur = e[pre[cur]].u;
    }
    return flow;
}
void isap() {
    bfs();
    ll flow = 0;
    int u = s;
    while (deep[s] <= n) {
        if (u == t) {
            flow += augement();
            u = s;
        }
        bool ok = 0;
        for (int i = now[u]; i; i = e[i].n) {
            int v = e[i].v;
            ll f = e[i].f;
            if (f && deep[v] + 1 == deep[u]) {
                ok = 1;
                now[u] = i;
                pre[v] = i;
                u = v;
                break;
            }
        }
        if (!ok) {
            if (!--gap[deep[u]]) break;
            int mindeep = n + 10;
            for (int i = head[u]; i; i = e[i].n) {
                int v = e[i].v;
                ll f = e[i].f;
                if (f) mindeep = min(mindeep, deep[v]);
            }
            deep[u] = mindeep + 1;
            gap[deep[u]]++;
            now[u] = head[u];
            if (u != s) u = e[pre[u]].u;
        }
        
    }
    cout << flow;
}
void solve() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; ++i) {
        int u, v;
        ll f;
        cin >> u >> v >> f;
        add(u, v, f);
        add(v, u, 0);
    }
    isap();
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

22.HLPP算法

P4722 【模板】最大流 加强版 / 预流推进
给定 n n n 个点, m m m 条有向边,给定每条边的容量,求从点 s s s 到点 t t t 的最大流。

O ( n 2 m ) O(n^2\sqrt m) O(n2m )

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 3e5 + 10;
struct edge {
    int u, v, n;
    ll f;
}e[N];
ll  flow[N], ex[N];
int n, m, s, t, cnt = 1, head[N], pre[N], now[N], gap[N], deep[N], inq[N];
struct cmp {
    bool operator()(const int a, const int b) const {
        return deep[a] < deep[b];
    }
};
int read() {
    int res = 0;
    char ch = getchar();
    while (ch < '0' || ch>'9') { ch = getchar(); }
    while (ch >= '0' && ch <= '9') { res = res * 10 + ch - '0'; ch = getchar(); }
    return res;
}
void add(int u, int v, ll f) {
    cnt++;
    e[cnt] = { u, v, head[u], f };
    head[u] = cnt;
}
void bfs() {
    for (int i = 1; i <= n; ++i) deep[i] = n + 1;
    deep[t] = 1;
    queue<int> q;
    q.push(t);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (register int i = head[u]; i; i = e[i].n) {
            int v = e[i].v;
            ll f = e[i ^ 1].f;
            if (f && deep[v] == n + 1) {
                deep[v] = deep[u] + 1;
                q.push(v);
            }
        }
    }
}
void hlpp() {
    bfs();
    priority_queue<int, vector<int>, cmp> q;
    deep[s] = n + 1;
    for (register int i = 1; i <= n; ++i)gap[deep[i]]++;
    for (register int i = head[s]; i; i = e[i].n) {
        int v = e[i].v;
        ll f = e[i].f;
        if (f) {
            e[i].f -= f;
            e[i ^ 1].f += f;
            ex[s] -= f;
            ex[v] += f;
            if (!inq[v] && v != s && v != t) {
                inq[v] = 1;
                q.push(v);
            }
        }
    }
    while (!q.empty()) {
        int u = q.top();
        q.pop();
        inq[u] = 0;
        for (register int i = head[u]; i; i = e[i].n) {
            int v = e[i].v;
            ll f = e[i].f;
            if (f && deep[v] + 1 == deep[u]) {
                int k = min(ex[u], f);
                e[i].f -= k;
                e[i ^ 1].f += k;
                ex[u] -= k;
                ex[v] += k;
                if (!inq[v] && v != s && v != t) {
                    inq[v] = 1;
                    q.push(v);
                }
                if (!ex[u]) break;
            }
        }
        if (ex[u]) {
            if (!--gap[deep[u]]) {
                for (register int i = 1; i <= n; i++) {
                    if (i != s && i != t && deep[i] > deep[u] && deep[i] < n + 1) {
                        deep[i] = n + 1;
                    }
                }
            }
            int mindeep = 0x3f3f3f3f;
            for (register int i = head[u]; i; i = e[i].n) {
                int v = e[i].v;
                ll f = e[i].f;
                if (f) mindeep = min(mindeep, deep[v]);
            }
            deep[u] = mindeep + 1;
            gap[deep[u]]++;
            inq[u] = 1;
            q.push(u);
        }
    }
    cout << ex[t];
}
void solve() {
    n = read(), m = read(), s = read(), t = read();
    for (register int i = 1; i <= m; ++i) {
        int u, v;
        ll f;
        u = read(), v = read(), f = read();
        add(u, v, f);
        add(v, u, 0);
    }
    hlpp();
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

23.二分图最大匹配

P3386 【模板】二分图最大匹配
给定一个二分图,其左部点的个数为 n n n,右部点的个数为 m m m,边数为 e e e,求其最大匹配的边数。
左部点从 1 1 1 n n n 编号,右部点从 1 1 1 m m m 编号。

匈牙利算法

O ( n m ) O(nm) O(nm)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 5e4 + 10;
int n, m, p,sum=0,match[N],vis[N];
vector<vector<int> > e(N);
bool dfs(int x) {
    for (int i:e[x]) {
        if (!vis[i]) {
            vis[i] = 1;
            if (!match[i] || dfs(match[i])) {
                match[i] = x;
                return 1;
            }
        }
    }
    return 0;
}
void solve() {
    cin >> n >> m >> p;
    for (int i = 1; i <= p; ++i) {
        int x, y;
        cin >> x >> y;
        e[x].push_back(y);
    }
    for (int i = 1; i <= n; ++i) {
        memset(vis, 0, sizeof(vis));
        if (dfs(i)) sum++;
    }
    cout << sum;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

最大流

思路:建一个点s编号为1为源点,建一个点t编号为n+m+2为汇点。然后让A类点分别编号为2 ∼ \sim n+1, B类点分别编号为n+2 ∼ \sim n+m+1。
然后建边:把s和所有A类点都连边,AB类点之间根据输入连边,所有B类点和T连边,每条边边权均为1.
最后在图上跑网络最大流即可。

O ( n m ) O(\sqrt nm) O(n m)

#include <bits/stdc++.h>
using namespace std;
const int N = 2005;
const int M = 1002005;
const int INF = 0x7fffffff;
struct edge{
    int v,val,nxt,rev;
};
edge e[M<<1];
int fe[N];
queue<int> q;
int dep[N];
int n,m,s,t;
bool bfs(){
    while(!q.empty()) q.pop();
    memset(dep,0,sizeof(dep));
    q.push(s); dep[s] = 1;
    while(!q.empty()){
        int c = q.front(); q.pop();
        for(int i=fe[c]; i; i=e[i].nxt){
            if(e[i].val>0 && !dep[e[i].v]){
                dep[e[i].v] = dep[c]+1;
                q.push(e[i].v); 
            }
        }
    }
    if(dep[t]) return true;
    else return false;
}
int dfs(int pos,int cur){
    if(pos==t) return cur;
    int rst = cur;
    for(int i=fe[pos]; i; i=e[i].nxt){
        if(dep[e[i].v]==dep[pos]+1 && e[i].val>0 && rst){
            int flow = dfs(e[i].v,min(e[i].val,rst));
            if(flow>0){
                e[i].val -= flow;
                rst -= flow;
                e[e[i].rev].val += flow;
            }
        }
    }
    return cur-rst;
}
int dinic(){
    int ans = 0;
    while(bfs()) ans += dfs(s,INF);
    return ans;
}
void addedge(int u,int v,int val,int rev){
    e[++m].v = v; e[m].val = val; e[m].rev = rev;
    e[m].nxt = fe[u]; fe[u] = m;
}
int main(){
    int n1,n2,m0; m = 0;
    scanf("%d%d%d",&n1,&n2,&m0);
    n = n1+n2+2;
    for(int i=1; i<=m0; i++){
        int x,y;
        scanf("%d%d",&x,&y);
        if(x<=n1 && y<=n2){
            addedge(x+1,y+n1+1,1,m+2);
            addedge(y+n1+1,x+1,0,m);
        }
    }
    for(int i=1; i<=n1; i++){
        addedge(1,i+1,1,m+2);
        addedge(i+1,1,0,m);
    }
    for(int i=1; i<=n2; i++){
        addedge(i+n1+1,n,1,m+2);
        addedge(n,i+n1+1,0,m);
    }
    s = 1; t = n;
    printf("%d\n",dinic()); 
    return 0;
}

24.二分图最大权匹配(KM算法)

P6577 【模板】二分图最大权完美匹配
给定一张二分图,左右部均有 n n n 个点,共有 m m m 条带权边,且保证有完美匹配。
求一种完美匹配的方案,使得最终匹配边的边权之和最大。
O ( n 3 ) O(n^3) O(n3)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 5e2 + 10;
int mx[N], my[N], pre[N], vx[N], vy[N], n, m;
ll slack[N], e[N][N], wx[N], wy[N], d;
void augement(int x) {
    while (x) {
        my[x] = pre[x];
        int t = mx[pre[x]];
        mx[pre[x]] = x;
        x = t;
    }
}
void bfs(int x) {
    queue<int> q;
    q.push(x);
    while (1) {
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            vx[u] = 1;
            for (int i = 1; i <= n; ++i) {
                if (!vy[i] && wx[u] + wy[i] - e[u][i] < slack[i]) {
                    slack[i] = wx[u] + wy[i] - e[u][i];
                    pre[i] = u;
                    if (!slack[i]) {
                        vy[i] = 1;
                        if (!my[i]) {
                            augement(i);
                            return;
                        }
                        else q.push(my[i]);
                    }
                }
            }
        }
        d = INF;
        for (int i = 1; i <= n; ++i)if (!vy[i]) d = min(d, slack[i]);
        for (int i = 1; i <= n; ++i) {
            if (vx[i]) wx[i] -= d;
            if (vy[i]) wy[i] += d;
            else slack[i] -= d;
        }
        for (int i = 1; i <= n; ++i) {
            if (!vy[i] && !slack[i]) {
                vy[i] = 1;
                if (!my[i]) {
                    augement(i);
                    return;
                }
                else q.push(my[i]);
            }
        }
    }
}
void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            e[i][j] = -INF;
        }
    }
    for (int i = 1; i <= m; ++i) {
        int x, y, w;
        cin >> x >> y >> w;
        e[x][y] = w;
        wx[x] = max(wx[x], 1ll * w);
    }
    for (int i = 1; i <= n; ++i) {
        memset(vx, 0, sizeof(vx));
        memset(vy, 0, sizeof(vy));
        fill(slack + 1, slack + n + 1, INF);
        bfs(i);
    }
    ll ans = 0;
    for (int i = 1; i <= n; ++i) ans += e[i][mx[i]];
    cout << ans << '\n';
    for (int i = 1; i <= n; ++i) cout << my[i] << " ";
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

25.一般图最大匹配

P6113 【模板】一般图最大匹配
给出一张 n n n 个点 m m m 条边的无向图,求该图的最大匹配。

26.最小费用最大流

P3381 【模板】最小费用最大流
给出一个包含 n n n 个点和 m m m 条边的有向图(下面称其为网络) G = ( V , E ) G=(V,E) G=(V,E),该网络上所有点分别编号为 1 ∼ n 1 \sim n 1n,所有边分别编号为 1 ∼ m 1\sim m 1m,其中该网络的源点为 s s s,汇点为 t t t,网络上的每条边 ( u , v ) (u,v) (u,v) 都有一个流量限制 w ( u , v ) w(u,v) w(u,v) 和单位流量的费用 c ( u , v ) c(u,v) c(u,v)
你需要给每条边 ( u , v ) (u,v) (u,v) 确定一个流量 f ( u , v ) f(u,v) f(u,v),要求:

  1. 0 ≤ f ( u , v ) ≤ w ( u , v ) 0 \leq f(u,v) \leq w(u,v) 0f(u,v)w(u,v)(每条边的流量不超过其流量限制);
  2. ∀ p ∈ { V ∖ { s , t } } \forall p \in \{V \setminus \{s,t\}\} p{V{s,t}} ∑ ( i , p ) ∈ E f ( i , p ) = ∑ ( p , i ) ∈ E f ( p , i ) \sum_{(i,p) \in E}f(i,p)=\sum_{(p,i)\in E}f(p,i) (i,p)Ef(i,p)=(p,i)Ef(p,i)(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
  3. ∑ ( s , i ) ∈ E f ( s , i ) = ∑ ( i , t ) ∈ E f ( i , t ) \sum_{(s,i)\in E}f(s,i)=\sum_{(i,t)\in E}f(i,t) (s,i)Ef(s,i)=(i,t)Ef(i,t)(源点流出的流量等于汇点流入的流量)。

定义网络 G G G 的流量 F ( G ) = ∑ ( s , i ) ∈ E f ( s , i ) F(G)=\sum_{(s,i)\in E}f(s,i) F(G)=(s,i)Ef(s,i),网络 G G G 的费用 C ( G ) = ∑ ( i , j ) ∈ E f ( i , j ) × c ( i , j ) C(G)=\sum_{(i,j)\in E} f(i,j) \times c(i,j) C(G)=(i,j)Ef(i,j)×c(i,j)
你需要求出该网络的最小费用最大流,即在 F ( G ) F(G) F(G) 最大的前提下,使 C ( G ) C(G) C(G) 最小。

思路:从零流开始,每次增加一个最小费用路径,经过多次增广,直到无法再增加路径,就得到了最大流。

O ( F n m ) O(Fnm) O(Fnm)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll INF = 1e18;
const ll N = 1e5 + 10;
struct edge {
    int u, v, f, c,n;
}e[N];
int head[N],pre[N],inq[N],dist[N],flow[N], cnt =1,n,m,s,t;
void add(int u, int v, int f, int c) {
    cnt++;
    e[cnt] = { u, v, f, c, head[u] };
    head[u] = cnt;
}
bool spfa() {
    memset(pre, 0, sizeof(pre));
    memset(inq, 0, sizeof(inq));
    memset(dist, 0x3f, sizeof(dist));
    memset(flow, 0x3f, sizeof(flow));
    dist[s] = 0;
    inq[s] = 1;
    queue<int> q;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        inq[u] = 0;
        for (int i = head[u]; i; i = e[i].n) {
            int v = e[i].v, f = e[i].f, c = e[i].c;
            if (f&&dist[v] > dist[u] + c) {
                dist[v] = dist[u] + c;
                pre[v] = i;
                flow[v] = min(flow[u], f);
                if (!inq[v]) {
                    inq[v] = 1;
                    q.push(v);
                }
            }
        }
    }
    if (dist[t] == 0x3f3f3f3f) return 0;
    else return 1;
}
void maxflow() {
    ll maxflow = 0, mincost = 0;
    while (spfa()) {
        int cur = t;
        while (cur != s) {
            e[pre[cur]].f -= flow[t];
            e[pre[cur] ^ 1].f += flow[t];
            cur = e[pre[cur]].u;
        }
        maxflow += flow[t];
        mincost += 1ll*dist[t]*flow[t];
    }
    cout << maxflow << " " << mincost;
}
void solve() {
    cin >> n >> m >> s >> t;
    for (int i = 1; i <= m; ++i) {
        int u, v, f, c;
        cin >> u >> v >> f >> c;
        add(u, v, f, c);
        add(v, u, 0, -c);
    }
    maxflow();
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

27.有源汇上下界最大流

P5192 Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流
在接下来的 n n n天中,射命丸文将要拍摄幻想乡的少女的照片并且从中为第 x x x个少女拍摄至少 G x G_x Gx张照片刊登在《文文。新闻》上。在第 k k k天的时候文文有 C k C_k Ck个取材对象,且对于每个取材对象拍的照片必须在闭区间 [ L k i , R k i ] [L_{k_i},R_{k_i}] [Lki,Rki]中。如果过少,文文就搞不出大新文;如果过多,就会有少女很安格瑞。
除此之外,因为拍照设备的原因,对于第 i i i天,每天的拍照数量不能超过 D i D_i Di张。在满足这些限定的条件下,文文希望拍到的照片尽可能地多。
由于文文需要去取材,因此她把这个任务交给了你,让你帮她去解决。

思路:
无源汇上下界可行流
(1)统计每个点x的 i n x in_x inx o u t x out_x outx ,即连向x的边的流量之和与连出x的边的流量之和。
(2)对于每一条原图里的边,流量设为上限减去下限,因此得到一张新图。
(3)虚拟源点与汇点s和t,对于所有的x,如果 i n x > o u t x in_x>out_x inx>outx,则从s向x连一条 i n x − o u t x in_x−out_x inxoutx的边,否则从x向t连一条 o u t x − i n x out_x−in_x outxinx的边。
(4)跑一遍从s到t的最大流,如果s连出的边可以满流,证明存在可行流。
有源汇上下界可行流
(1)从原图中的汇点向源点连一条上限为inf的边。
(2)跑一遍无源汇上下界可行流。
有源汇上下界最大流
(1)跑出一个有源汇上下界可行流,如果满流则设其答案为flow1,否则不存在答案。
(2)删去从原图汇点到原图源点的边。
(3)跑从原图源点到原图汇点的最大流,设答案为flow2,则总答案ans=flow1+flow2。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const int INF = 1e9;
const ll N = 1e6 + 10;
struct edge {
    int u, v, f, n;
}e[N];
int n, m, s1, s2, t1, t2, cnt, head[N],deep[N],gap[N],now[N],pre[N],fl[N];
void add(int u, int v, int f) {
    cnt++;
    e[cnt] = { u, v, f, head[u] };
    head[u] = cnt;
}
void bfs(int s,int t) {
    memcpy(now, head, sizeof(head));
    memset(deep, 0, sizeof(deep));
    memset(gap, 0, sizeof(gap));
    deep[t] = 1;
    queue<int> q;
    q.push(t);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        gap[deep[u]]++;
        for (int i = head[u]; i; i = e[i].n) {
            int v = e[i].v, f = e[i^1].f;
            if (f && !deep[v]) {
                deep[v] = deep[u] + 1;
                q.push(v);
            }
        }
    }
}
int augement(int s,int t) {
    int flow = INF, cur = t;
    while (cur != s) {
        flow = min(flow, e[pre[cur]].f);
        cur = e[pre[cur]].u;
    }
    cur = t;
    while (cur != s) {
        e[pre[cur]].f -= flow;
        e[pre[cur] ^ 1].f += flow;
        cur = e[pre[cur]].u;
    }
    return flow;
}
ll isap(int s, int t) {
    bfs(s,t);
    ll flow = 0;
    int u = s;
    while (deep[s] <= n + m + 4) {
        if (u == t) {
            flow += augement(s,t);
            u = s;
        }
        bool ok = 0;
        for (int i = now[u]; i; i = e[i].n) {
            int v = e[i].v, f = e[i].f;
            if (f&&deep[v]+1==deep[u]) {
                now[u] = i;
                pre[v] = i;
                ok = 1;
                u = v;
                break;
            }
        }
        if (!ok) {
            if (!--gap[deep[u]]) break;
            int mindeep = n+m+10;
            for (int i = head[u]; i; i = e[i].n) {
                int v = e[i].v, f = e[i].f;
                if (f) mindeep = min(mindeep, deep[v]);
            }
            deep[u] = mindeep+1;
            gap[deep[u]]++;
            now[u] = head[u];
            if(u!=s) u = e[pre[u]].u;
        }
    }
    return flow;
}
void solve() {
    while (cin >> n >> m) {
        memset(fl, 0, sizeof(fl));
        memset(head, 0, sizeof(head));
        s1 = n + m + 1, t1 = n + m + 2, s2 = n + m + 3, t2 = n + m + 4,cnt=1;
        ll sum = 0;
        for (int i = 1; i <= m; ++i) {
            int g;
            cin >> g;
            add(n + i, t1,INF);
            add(t1, n + i,0);
            fl[n + i] -= g, fl[t1] += g;
        }
        for (int i = 1; i <= n; ++i) {
            int c, d;
            cin >> c >> d;
            add(s1, i, d);
            add(i, s1, 0);
            for (int j = 1; j <=c; ++j) {
                int t, l, r;
                cin >> t >> l >> r;
                t++;
                add(i, n + t, r-l);
                add(n + t, i, 0);
                fl[i] -= l, fl[n + t] += l;
            }
        }
        for (int i = 1; i <= n + m + 2; ++i) {
            if (fl[i] < 0) {
                add(i, t2, -fl[i]);
                add(t2, i, 0);
            }
            else {
                add(s2, i, fl[i]);
                add(i,s2,0);
                sum += fl[i];
            }
        }
        add(t1, s1,INF);
        add(s1, t1, 0);
        if (isap(s2, t2) != sum) {
            cout << -1 << "\n\n";
            continue;
        }
        e[cnt].f = e[cnt ^ 1].f = 0;
        cout << sum + isap(s1, t1) << "\n\n";
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

28.最小树形图(朱刘算法)

P4716 【模板】最小树形图
给定包含 n n n 个结点, m m m 条有向边的一个图。试求一棵以结点 r r r 为根的最小树形图,并输出最小树形图每条边的权值之和,如果没有以 r r r 为根的最小树形图,输出 − 1 -1 1

O ( n m ) O(nm) O(nm)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e9;
const ll N = 1e2 + 10;
stack<int> st;
int n, m, r;
int d[N][N], bd[N][N], num[N], low[N], sccno[N], vis[N], pre[N], dfn, cnt;
void dfs(int x) {
    vis[x] = 1;
    for (int i = 1; i <= n; ++i) {
        if (d[x][i] != INF && !vis[i]) dfs(i);
    }
}
void tarjan(int x) {
    st.push(x);
    num[x] = low[x] = ++dfn;
    if (num[pre[x]] == 0) {
        tarjan(pre[x]);
        low[x] = min(low[x], low[pre[x]]);
    }
    else if (sccno[pre[x]] == 0) low[x] = min(low[x], num[pre[x]]);
    if (low[x] == num[x]) {
        cnt++;
        int u = -1;
        while (u != x) {
            u = st.top();
            st.pop();
            sccno[u] = cnt;
        }
    }
}
void solve() {
    cin >> n >> m >> r;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) d[i][j] = INF;
    }
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        if (u != v && v != r) d[u][v] = min(d[u][v], w);
    }
    dfs(r);
    for (int i = 1; i <= n; ++i) {
        if (!vis[i]) {
            cout << -1;
            return;
        }
    }
    ll res = 0;
    while (1) {
        for (int i = 1; i <= n; ++i) {
            pre[i] = i;
            for (int j = 1; j <= n; ++j) {
                if (d[j][i] < d[pre[i]][i]) pre[i] = j;
            }
        }
        memset(num, 0, sizeof(num));
        memset(sccno, 0, sizeof(sccno));
        dfn = cnt = 0;
        for (int i = 1; i <= n; ++i) {
            if (!num[i]) tarjan(i);
        }
        if (cnt == n) {
            for (int i = 1; i <= n; ++i) {
                if (i == r) continue;
                res += d[pre[i]][i];
            }
            break;
        }
        else {
            for (int i = 1; i <= n; ++i) {
                if (i == r) continue;
                if (sccno[i] == sccno[pre[i]]) res += d[pre[i]][i];
            }
            for (int i = 1; i <= cnt; ++i) {
                for (int j = 1; j <= cnt; ++j) bd[i][j] = INF;
            }
            for (int i = 1; i <= n; ++i) {
                for (int j = 1; j <= n; ++j) {
                    if (d[i][j] != INF && sccno[i] != sccno[j]) {
                        if (sccno[pre[j]] == sccno[j]) bd[sccno[i]][sccno[j]] = min(bd[sccno[i]][sccno[j]], d[i][j] - d[pre[j]][j]);
                        else bd[sccno[i]][sccno[j]] = min(bd[sccno[i]][sccno[j]], d[i][j]);
                    }
                }
            }
        }
        r = sccno[r];
        n = cnt;
        memcpy(d, bd, sizeof(bd));
    }
    cout << res;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

29.Prufer序列

P6086 【模板】Prufer 序列
请实现 Prüfer 序列和无根树的相互转化。
为方便你实现代码,尽管是无根树,我们在读入时仍将 n n n 设为其根。
对于一棵无根树,设 f 1 … n − 1 f_{1\dots n-1} f1n1 为其父亲序列 f i f_i fi 表示 i i i n n n 为根时的父亲),设 p 1 … n − 2 p_{1 \dots n-2} p1n2 为其 Prüfer 序列
另外,对于一个长度为 m m m 的序列 a 1 … m a_{1 \dots m} a1m,我们设其权值 xor ⁡ i = 1 m i × a i \operatorname{xor}_{i = 1}^m i \times a_i xori=1mi×ai

O ( n ) O(n) O(n)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e9;
const ll N = 5e6 + 10;
int f[N], p[N], out[N];
void solve() {
    int n, op;
    ll res = 0;
    cin >> n >> op;
    if (op == 1) {
        for (int i = 1; i <= n - 1; ++i) {
            cin >> f[i];
            out[f[i]]++;
        }
        for (int i = 0, j = 1; i <= n - 2; ++j) {
            while (out[j]) j++;
            p[++i] = f[j];
            while (i <= n - 2 && --out[p[i]] == 0 && p[i] < j) p[i + 1] = f[p[i]], i++;
        }
        for (int i = 1; i <= n - 2; ++i) res ^= 1ll * i * p[i];
    }
    else {
        for (int i = 1; i <= n - 2; ++i) {
            cin >> p[i];
            out[p[i]]++;
        }
        p[n - 1] = n;
        out[p[n - 1]]++;
        for (int i = 0, j = 1; i <= n - 1; ++j) {
            while (out[j]) j++;
            f[j] = p[++i];
            while (i <= n - 1 && --out[p[i]] == 0 && p[i] < j) f[p[i]] = p[i + 1], i++;
        }
        for (int i = 1; i <= n - 1; ++i) res ^= 1ll * i * f[i];
    }
    cout << res;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

Cayley 公式:完全图有 n n − 2 n^{n-2} nn2棵生成树。

30.仙人掌

P5236 【模板】静态仙人掌
任意一条边至多只出现在一条简单回路的无向连通图称为仙人掌。
给你一个有 n n n 个点和 m m m 条边的仙人掌图,和 q q q 组询问
每次询问两个点 u , v u,v u,v,求两点之间的最短路。

O ( l o g n ) O(logn) O(logn)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 2e4 + 10;
struct edge {
    int u, v, w;
};
vector<vector<edge> >e(N);
vector<vector<edge> >p(N);
int newn, s[N];
int num[N], low[N], dfn, fa[N], fw[N];
int f[N][15], deep[N], dis[N], A, B;
void build(int u, int v, int w) {
    newn++;
    int tmp = v, sum = w;
    while (tmp != u) {
        s[tmp] = sum;
        sum += fw[tmp];
        tmp = fa[tmp];
    }
    s[newn] = s[u] = sum;
    tmp = v;
    while (tmp != u) {
        p[newn].push_back({ newn,tmp,min(s[tmp],s[u] - s[tmp]) });
        tmp = fa[tmp];
    }
    p[u].push_back({ u,newn,0 });
}
void tarjan(int x, int father) {
    num[x] = low[x] = ++dfn;
    for (auto i : e[x]) {
        int v = i.v, w = i.w;
        if (!num[v]) {
            fa[v] = x, fw[v] = w;
            tarjan(v, x);
            low[x] = min(low[x], low[v]);
            if (num[x] < low[v])  p[x].push_back({ x,v,w });
        }
        else if (v != father) low[x] = min(low[x], num[v]);
    }
    for (auto i : e[x]) {
        int v = i.v, w = i.w;
        if (num[x] < num[v] && fa[v] != x) build(x, v, w);
    }
}
void dfs(int x, int father) {
    deep[x] = deep[father] + 1;
    f[x][0] = father;
    for (int i = 1; (1 << i) <= deep[x]; ++i) f[x][i] = f[f[x][i - 1]][i - 1];
    for (auto i : p[x]) {
        int v = i.v;
        dis[v] = dis[x] + i.w;
        if (v != father) dfs(v, x);
    }
}
int lca(int x, int y) {
    if (deep[x] < deep[y]) swap(x, y);
    for (int i = 14; i >= 0; --i) {
        if (deep[f[x][i]] >= deep[y]) x = f[x][i];
    }
    if (x == y) return x;
    for (int i = 14; i >= 0; --i) {
        if (f[x][i] != f[y][i]) {
            x = f[x][i];
            y = f[y][i];
        }
    }
    A = x, B = y;
    return f[x][0];
}
void solve() {
    int n, m, q;
    cin >> n >> m >> q;
    newn = n;
    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        e[u].push_back({ u,v,w });
        e[v].push_back({ v,u,w });
    }
    tarjan(1, 0);
    dfs(1, 0);
    for (int i = 1; i <= q; ++i) {
        int a, b;
        cin >> a >> b;
        int d = lca(a, b);
        if (d <= n) cout << dis[a] + dis[b] - dis[d] * 2 << '\n';
        else cout << dis[a] + dis[b] - dis[A] - dis[B] + min(abs(s[A] - s[B]), s[d] - abs(s[A] - s[B])) << '\n';
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

31.最小斯坦纳树

P6192 【模板】最小斯坦纳树
给定一个包含 n n n 个结点和 m m m 条带权边的无向连通图 G = ( V , E ) G=(V,E) G=(V,E)
再给定包含 k k k 个结点的点集 S S S,选出 G G G 的子图 G ′ = ( V ′ , E ′ ) G'=(V',E') G=(V,E),使得:

  1. S ⊆ V ′ S\subseteq V' SV
  2. G ′ G' G 为连通图;
  3. E ′ E' E 中所有边的权值和最小。

你只需要求出 E ′ E' E 中所有边的权值和。

答案的子图一定是树,为这棵树钦定一个树根,设 dp(i,S) 表示以 i 为根的一棵树,包含集合 S 中所有点的最小代价。
一棵以 i 为根的树有两种情况,第一种是 i 的度数为 1,另一种是 i 的度数大于 1。
对于 i 的度数为 1 的情况,可以考虑枚举树上与 i 相邻的点 j,则:dp(j,S)+w(j,i)→dp(i,S)
对于 i 的度数大于 1 的情况,可以划分成几个子树考虑,即:dp(i,T)+dp(i,S−T)→dp(i,S) (T⊆S)
O ( n 3 k + m log ⁡ m 2 k ) O(n3^k+m\log m2^k) O(n3k+mlogm2k)

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<ll,ll>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e2 + 10;
struct edge {
    ll u, v, w;
};
vector<vector<edge> >e(N);
ll n, m, k, vis[N], dp[N][1 << 10];//树以i为根时包含s中所有点的最小代价
void dijk(ll sta) {
    memset(vis, 0, sizeof(vis));
    priority_queue<P, vector<P>, greater<P> >q;
    for (int i = 1; i <= n; ++i) q.push({ dp[i][sta],i });
    while (!q.empty()) {
        int u = q.top().second;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto i : e[u]) {
            if (dp[i.v][sta] > dp[u][sta] + i.w) {
                dp[i.v][sta] = dp[u][sta] + i.w;
                q.push({ dp[i.v][sta], i.v });
            }
        }
    }
}
inline void solve() {
    cin >> n >> m >> k;
    for (int i = 1; i <= m; ++i) {
        ll u, v, w;
        cin >> u >> v >> w;
        e[u].push_back({ u,v,w });
        e[v].push_back({ v,u,w });
    }
    memset(dp, 0x7f, sizeof(dp));
    for (int i = 1; i <= k; ++i) {
        ll s;
        cin >> s;
        dp[s][1 << (i - 1)] = 0;
    }
    for (int i = 1; i < (1 << k); ++i) {
        for (int j = (i - 1) & i; j; j = (j - 1) & i) {//i的度数大于1,划分成几个子树,枚举子集
            for (int t = 1; t <= n; ++t) {
                dp[t][i] = min(dp[t][i], dp[t][j] + dp[t][i ^ j]);
            }
        }
        dijk(i);//i的度数为1时,枚举相邻点
    }
    ll ans = INF;
    for (int i = 1; i <= n; ++i) ans = min(ans, dp[i][(1 << k) - 1]);
    cout << ans;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t = 1;
    //_t = read();
    while (_t--) {
        solve();
    }
    return 0;
}

32.矩阵树定理

经典
给出一个无向无权图,设 A 为邻接矩阵, D 为度数矩阵( D[i][i]=节点 i 的度数,其他的无值)。则基尔霍夫矩阵即为 : K=D−A。然后令 K′ 为 K 去掉第k行与第k列(k任意)的结果(n−1阶主子式) 即为该图的生成树个数。
P4336 [SHOI2016]黑暗前的幻想乡

加权扩展
带重边的情况,上面的经典矩阵树定理也是能够处理的。根据乘法原理,对于某种生成树的形态,其贡献为每条边重的次数的乘积。如果把重边次数理解成权值的话,那么矩阵树定理求的就是 : 所有生成树边权乘积的总和。(这里注意度数矩阵变成了相邻边的权值和)
P3317 [SDOI2014]重建

有向扩展
邻接矩阵 A 的意义同有向图邻接矩阵,那么现在的矩阵 D 就要变一下了。若 D [ i ] [ i ] = ∑ j = 1 n A [ j ] [ i ] D[i][i]= \sum\limits_{j=1}^n A[j][i] D[i][i]=j=1nA[j][i] ,即到该点的边权总和(入)。此时求的就是外向树 (从根向外)。若 D [ i ] [ i ] = ∑ j = 1 n A [ i ] [ j ] D[i][i]= \sum\limits_{j=1}^n A[i][j] D[i][i]=j=1nA[i][j] ,即从从该点出发的边权总和(出)。此时求的就是内向树 (从外向根)(同样可以加权!)
此外,既然是有向的,那么就需要指定根。前面提过要任意去掉第 k 行与第 k 列,是因为无向图所以不用在意谁为根。在有向树的时候需要理解为指定根,结论是 : 去掉哪一行就是那一个元素为根。
P4455 [CQOI2018] 社交网络

P6178 【模板】Matrix-Tree 定理
给定一张 n n n 个结点 m m m 条边的带权图(可能为无向图,可能为有向图)。
定义其一个生成树 T T T 的权值为 T T T 中所有边权的乘积。
求其所有不同生成树的权值之和,对 1 0 9 + 7 10^9+7 109+7 取模。
注意:

  1. 本题中,有向图的生成树指的是 1 1 1 为根的外向树
  2. 两棵生成树 T 1 , T 2 T_1,T_2 T1,T2 不同,当且仅当存在存在一条边 e e e,满足 e ∈ T 1 ,    e ∉ T 2 e\in T_1,\ \ e\notin T_2 eT1,  e/T2
#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<ll,ll>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e3 + 10;
ll n, m, t, a[N][N];
ll ksm(ll a, ll b, ll p) {
    a %= p;
    ll res = 1;
    while (b) {
        if (b & 1) res = res * a % p;
        b >>= 1;
        a = a * a % p;
    }
    return res;
}
ll cal() {
    ll f = 1;
    for (int i = 2; i <= n; ++i) {
        for (int j = i + 1; j <= n; ++j) {
            if (!a[i][i] && a[j][i]) {
                swap(a[i], a[j]);
                f = -f;
                break;
            }
        }
        ll t = ksm(a[i][i], mod - 2, mod);
        for (int j = i + 1; j <= n; ++j) {
            for (int k = n; k >= i; --k) {
                a[j][k] = (a[j][k] - t * a[j][i] % mod * a[i][k] % mod + mod) % mod;
            }
        }
    }
    ll res = 1;
    for (int i = 2; i <= n; ++i) res = res * a[i][i] % mod;
    return (res * f % mod + mod) % mod;
}
inline void solve() {
    cin >> n >> m >> t;
    for (int i = 1; i <= m; ++i) {
        ll u, v, w;
        cin >> u >> v >> w;
        if (!t) {
            a[u][v] = (a[u][v] - w + mod) % mod, a[v][u] = (a[v][u] - w + mod) % mod;
            a[u][u] = (a[u][u] + w) % mod, a[v][v] = (a[v][v] + w) % mod;
        }
        else {
            a[u][v] = (a[u][v] - w + mod) % mod;
            a[v][v] = (a[v][v] + w) % mod;
        }
    }
    cout << cal();
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t = 1;
    //_t = read();
    while (_t--) {
        solve();
    }
    return 0;
}

33.双端队列

边权为0或1

P4667 [BalticOI 2011 Day1] Switch the Lamp On
Casper 正在设计电路。有一种正方形的电路元件,在它的两组相对顶点中,有一组会用导线连接起来,另一组则不会。有 N × M N\times M N×M 个这样的元件,你想将其排列成 N N N 行,每行 M M M 个。 电源连接到板的左上角。灯连接到板的右下角。只有在电源和灯之间有一条电线连接的情况下,灯才会亮着。为了打开灯,任何数量的电路元件都可以转动 90°(两个方向)。
现在请你编写一个程序,求出最小需要多少旋转多少电路元件。

#include <bits/stdc++.h>
using namespace std;
#define LL long long 
#define P pair<int,int>
const double eps = 1e-8;
const l INF = 1e18;
const LL N = 5e2 + 10;
struct node {
    int x, y, dis;
};
int n, m;
int g[N][N], dis[N][N];
int d[4][2] = { {-1,-1},{-1,1},{1,-1},{1,1} }, ab[4] = { 2,1,1,2 }, cd[4][2] = { {-1,-1},{-1,0},{0,-1},{0,0} };
void bfs() {
    memset(dis, 0x3f, sizeof(dis));
    dis[1][1] = 0;
    deque<node> dq;
    dq.push_back({ 1,1,0 });
    while (!dq.empty()) {
        node u = dq.front();
        dq.pop_front();
        for (int i = 0; i < 4; ++i) {
            int nx = u.x + d[i][0], ny = u.y + d[i][1];
            int k = g[u.x + cd[i][0]][u.y + cd[i][1]] != ab[i];
            if (nx >= 1 && nx <= n + 1 && ny >= 1 && ny <= m + 1 && dis[nx][ny] > dis[u.x][u.y] + k) {
                dis[nx][ny] = dis[u.x][u.y] + k;
                if (k == 0) dq.push_front({ nx,ny,dis[nx][ny] });
                else dq.push_back({ nx,ny,dis[nx][ny] });
                if (nx == n + 1 && ny == m + 1) break;
            }
        }
    }
}
void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            char ch;
            cin >> ch;
            if (ch == '/') g[i][j] = 1;
            else g[i][j] = 2;
        }
    }
    bfs();
    if (dis[n + 1][m + 1] == 0x3f3f3f3f) cout << "NO SOLUTION";
    else cout << dis[n + 1][m + 1];
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

34.模拟退火

P4044 [AHOI2014/JSOI2014] 保龄球
一场保龄球比赛一共有 n n n 个轮次,每一轮都会有十个木瓶放置在木板道的另一端。每一轮中,选手都有两次投球的机会来尝试击倒全部的十个木瓶。对于每一次投球机会,选手投球的得分等于这一次投球所击倒的木瓶数量。选手每一轮的得分是他两次机会击倒全部木瓶的数量。
对于每一个轮次,有如下三种情况:

  1. “全中”:如果选手第一次尝试就击倒了全部十个木瓶,那么这一轮就为“全中”。在一个“全中”轮中,由于所有木瓶在第一次尝试中都已经被击倒,所以选手不需要再进行第二次投球尝试。同时,在计算总分时,选手在下一轮的得分将会被乘2计入总分。
  2. “补中”:如果选手使用两次尝试击倒了十个木瓶,那么这一轮就称为“补中”。同时,在计算总分时,选手在下一轮中的第一次尝试的得分将会以双倍计入总分。
  3. “失误”:如果选手未能通过两次尝试击倒全部的木瓶,那么这一轮就被称为“失误”。同时,在计算总分时,选手在下一轮的得分会被计入总分,没有分数被翻倍。此外,如果第 n n n 轮是“全中”,那么选手可以进行一次附加轮:也就是,如果第 n n n 轮是“全中”,那么选手将一共进行 n + 1 n+1 n+1 轮比赛。显然,在这种情况下,第 n + 1 n+1 n+1 轮的分数一定会被加倍。

附加轮的规则只执行一次。也就是说,即使第 n + 1 n+1 n+1 轮选手又打出了“全中”,也不会进行第 n + 2 n+2 n+2 轮比赛。因而,附加轮的成绩不会使得其他轮的分数翻番。最后,选手的总得分就是附加轮规则执行过,并且分数按上述规则加倍后的每一轮分数之和。
JYY 刚刚进行了一场 n n n 个轮次的保龄球比赛,但是,JYY非常不满意他的得分。JYY想出了一个办法:他可以把记分表上,他所打出的所有轮次的顺序重新排列,这样重新排列之后,由于翻倍规则的存在,JYY就可以得到更高的分数了!
当然了,JYY不希望做的太假,他希望保证重新排列之后,所需要进行的轮数和重排前所进行的轮数是一致的:比如如果重排前JYY在第 n n n 轮打出了“全中”,那么重排之后,第 n n n 轮还得是“全中”以保证比赛一共进行 n + 1 n+1 n+1 轮;同样的,如果 JYY 第 n n n 轮没有打出“全中”,那么重排过后第 n n n 轮也不能是全中。请你帮助 JYY 计算一下,他可以得到的最高的分数。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e2 + 10;
struct {
    int x, y;
}a[N];
int n, f = 0, ans = 0;
int calc() {
    int res = 0;
    for (int i = 0; i < n; ++i) {
        res += a[i].x + a[i].y;
        if (i < n - 1) {
            if (a[i].x == 10) res += a[i + 1].x + a[i + 1].y;
            else if (a[i].x + a[i].y == 10) res += a[i + 1].x;
        }
    }
    ans = max(ans, res);
    return res;
}
void sa() {
    for (double t = 1e4; t > 1e-4; t *= 0.99) {
        int d1 = calc();
        int x = rand() % n, y = rand() % n;
        swap(a[x], a[y]);
        if (f && a[n - 2].x != 10 || !f && a[n - 1].x == 10) swap(a[x], a[y]);
        else {
            int d2 = calc();
            if (exp((d2 - d1) / t) < (double)rand() / RAND_MAX) swap(a[x], a[y]);
        }
    }
}
void solve() {
    cin >> n;
    for (int i = 0; i < n; ++i) cin >> a[i].x >> a[i].y;
    if (a[n - 1].x == 10) {
        n++;
        f = 1;
        cin >> a[n - 1].x >> a[n - 1].y;
    }
    while ((double)clock() / CLOCKS_PER_SEC < 0.8) sa();
    cout << ans;
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

35.爬山法

P4035 [JSOI2008] 球形空间产生器
有一个球形空间产生器能够在 n n n 维空间中产生一个坚硬的球体。现在,你被困在了这个 n n n 维球体中,你只知道球面上 n + 1 n+1 n+1 个点的坐标,你需要以最快的速度确定这个 n n n 维球体的球心坐标,以便于摧毁这个球形空间产生器。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define P pair<int,int>
const double eps = 1e-8;
const ll mod = 1e9 + 7;
const ll INF = 1e18;
const ll N = 1e1 + 10;
int n;
double a[N][N], ans[N], dis[N], delta[N];
void calc() {
    double avg = 0;
    for (int i = 1; i <= n + 1; ++i) {
        dis[i] = delta[i] = 0;
        for (int j = 1; j <= n; ++j) {
            dis[i] += (a[i][j] - ans[j]) * (a[i][j] - ans[j]);
        }
        dis[i] = sqrt(dis[i]);
        avg += dis[i] / (n + 1);
    }
    for (int i = 1; i <= n + 1; ++i) {
        for (int j = 1; j <= n; ++j) {
            delta[j] += (dis[i] - avg) * (a[i][j] - ans[j]) / avg;
        }
    }
}
void solve() {
    cin >> n;
    for (int i = 1; i <= n + 1; ++i) {
        for (int j = 1; j <= n; ++j) {
            cin >> a[i][j];
            ans[j] += a[i][j] / (n + 1);
        }
    }
    for (double t = 1e4; t > 1e-4; t *= 0.99995) {
        calc();
        for (int i = 1; i <= n; ++i) ans[i] += delta[i] * t;
    }
    for (int i = 1; i <= n; ++i) cout << setiosflags(ios::fixed) << setprecision(3) << ans[i] << " ";
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int _t;
    _t = 1;
    //cin>>_t;
    while (_t--) {
        solve();
    }
    return 0;
}

36.概念

  1. 简单图 (simple graph):若一个图中没有自环和重边,它被称为简单图。具有至少两个顶点的简单无向图中一定存在度相同的结点。
  2. 若对 H ⊆ G H \subseteq G HG,满足 ∀ u , v ∈ V ′ \forall u, v \in V' u,vV,只要 ( u , v ) ∈ E (u, v) \in E (u,v)E,均有 ( u , v ) ∈ E ′ (u, v) \in E' (u,v)E,则称 H 是 G 的 导出子图/诱导子图 (induced subgraph)。
  3. H ⊆ G H \subseteq G HG 满足 V ′ = V V' = V V=V,则称 H 为 G 的 生成子图/支撑子图 (spanning subgraph)。
  4. 如果有向图 G = ( V , E ) G = (V, E) G=(V,E) 的导出子图 H = G [ V ∗ ] H = G \left[ V^\ast \right] H=G[V] 满足 ∀ v ∈ V ∗ , ( v , u ) ∈ E \forall v \in V^\ast, (v, u) \in E vV,(v,u)E,有 u ∈ V ∗ u \in V^\ast uV,则称 H 为 G 的一个 闭合子图 (closed subgraph)。
  5. 补图:对于无向简单图 G = ( V , E ) G = (V, E) G=(V,E),它的 补图 (complement graph) 指的是这样的一张图:记作 G ˉ \bar G Gˉ,满足 V ( G ˉ ) = V ( G ) V \left( \bar G \right) = V \left( G \right) V(Gˉ)=V(G),且对任意节点对 ( u , v ) (u, v) (u,v) ( u , v ) ∈ E ( G ˉ ) (u, v) \in E \left( \bar G \right) (u,v)E(Gˉ) 当且仅当 ( u , v ) ∉ E ( G ) (u, v) \notin E \left( G \right) (u,v)/E(G)
  6. 反图:对于有向图 G = ( V , E ) G = (V, E) G=(V,E),它的 反图 (transpose graph) 指的是点集不变,每条边反向得到的图,即:若 G 的反图为 G ′ = ( V , E ′ ) G'=(V, E') G=(V,E),则 E ′ = { ( v , u ) ∣ ( u , v ) ∈ E } E'=\{(v, u)|(u, v)\in E\} E={(v,u)(u,v)E}
  7. 竞赛图是定义在有向图上的概念,图中每对不同的顶点通过单个有向边连接,即每对顶点间都有一条有向边。对于 n 阶竞赛图,当 n 大于等于 2 时一定存在哈密顿通路。
    兰道定理是用来判定竞赛图的定理。将一个竞赛图的每一个点的出度从小到大排序后得到的序列称为竞赛图的比分序列。兰道定理的内容为:对于一个长度为 n 的序列 S = ( s 1 ⩽ s 2 ⩽ . . . ⩽ s n ) , n ⩾ 1 S=(s_1⩽s_2⩽...⩽s_n),n⩾1 S=(s1s2...sn),n1 是合法的比分序列,当且仅当: ∀ 1 ⩽ k ⩽ n , ∑ i = 1 k s i ⩾ ( k 2 ) ∀1⩽k⩽n,\sum\limits_{i=1}^ks_i⩾\binom{k}{2} ∀1kn,i=1ksi(2k),且 k=n 时,式子必须要取等
  8. 支配集:对于无向图 G = ( V , E ) G=(V, E) G=(V,E),若 V ′ ⊆ V V'\subseteq V VV ∀ v ∈ ( V ∖ V ′ ) \forall v\in(V\setminus V') v(VV)存在边 ( u , v ) ∈ E (u, v)\in E (u,v)E满足 u ∈ V ′ u\in V' uV,则 V ′ V' V 是图 G 的一个 支配集 (dominating set)。
  9. 边支配集:对于图 G=(V, E),若 E ′ ⊆ E 且 ∀ e ∈ ( E ∖ E ′ ) E'\subseteq E 且 \forall e\in(E\setminus E') EEe(EE) 存在 E ′ E' E 中的边与其有公共点,则称 E ′ E' E 是图 G 的一个 边支配集 (edge dominating set)。
  10. 独立集:对于图 G = ( V , E ) G=(V, E) G=(V,E),若 V ′ ⊆ V V'\subseteq V VV V ′ V' V 中任意两点都不相邻,则 V ′ V' V 是图 G 的一个 独立集 (independent set)。
  11. 匹配:对于图 G = ( V , E ) G=(V, E) G=(V,E),若 E ′ ∈ E E'\in E EE 且 E’ 中任意两条不同的边都没有公共的端点,且 E’ 中任意一条边都不是自环,则 E’ 是图 G 的一个 匹配 (matching),也可以叫作边独立集 (independent edge set)
  12. 点覆盖:对于图 G = ( V , E ) G=(V, E) G=(V,E),若 V ′ ⊆ V V'\subseteq V VV ∀ e ∈ E \forall e\in E eE 满足 e e e 的至少一个端点在 V ′ V' V 中,则称 V ′ V' V 是图 G G G 的一个 点覆盖 (vertex cover)。
  13. 边覆盖:对于图 G = ( V , E ) G=(V, E) G=(V,E),若 E ′ ⊆ E 且 ∀ v ∈ V E'\subseteq E 且 \forall v\in V EEvV 满足 v v v E ′ E' E 中的至少一条边相邻,则称 E ′ E' E 是图 G 的一个 边覆盖 (edge cover)。最大匹配 + 最小边覆盖 = n。
  14. 团:对于图 G = ( V , E ) G=(V, E) G=(V,E),若 V ′ ⊆ V V'\subseteq V VV V ′ V' V 中任意两个不同的顶点都相邻,则 V ′ V' V 是图 G G G 的一个 团 (clique)。团的导出子图是完全图。如果一个团在加入任何一个顶点后都不再是一个团,则这个团是一个 极大团 (maximal clique)。一张图的最大团的大小记作 ω ( G ) \omega(G) ω(G),最大团的大小等于其补图最大独立集的大小,即 ω ( G ) = α ( G ˉ ) \omega(G)=\alpha(\bar{G}) ω(G)=α(Gˉ)
  15. 最小路径覆盖:在DAG上,用最少的互不相交的路径,将所有点覆盖(互不相交指的是任意两个路径无重复点和重复边)(数值是路径数目)
    最小路径覆盖的值=n-最大匹配数
  16. 最小路径重复点覆盖:路径点重复,先求传递闭包,再做最小路径覆盖
  17. 结论
    最大点独立集 = 最小边覆盖 = 最小路径覆盖的值 = 补图最大团大小 = n - 最大匹配数 = n - 最小点覆盖集
    在二分图中,最大流 = 最小割 = 最小点权覆盖集 = n - 最大点权独立集,点权为1时,最大匹配数 = 最小点权覆盖集 = n - 最大点权独立集
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值