【2024.7.31】Traveller解题报告——网络流

洛谷 P2472 [SCOI2007] 蜥蜴

写在前面

这是本蒟蒻第一次用csdn写解题报告,写的不太好,不便之处请各位谅解~

今天学习了一下有关网络流的基础知识,感觉网络流真的是一个很高深的东西,并不是那么好理解,求解最大流的算法又有很多,最长用的有Dinic算法,ISAP算法等等。当然还有一些预流推进之类的“高科技”算法,但是我觉得我可能一时半会还是用不大,因此我可能也暂时不会去学它。

感觉网络流最关键的还是如何把一个问题转化为网络流的问题去求解它,俗称“建图”,把实际问题建模为图论中的网络流问题,然后使用网络流算法进行求解。例如接下来要讲的这个题目便是我做的第一道最大流建模问题。

题目大意

给你一个 n × m n\times m n×m的矩阵,其中为0的位置没有石柱,大于0的部分是一个高度为 a i j a_{ij} aij的石柱,这些石柱上面有若干个蜥蜴,这些蜥蜴需要从石柱上跳到矩阵外面,也就是跳出这个 n × m n \times m n×m的矩阵。限制条件是:每当一个蜥蜴从一个石柱上跳向另一个石柱时,其原本所在的石柱高度便会-1。若这个石柱高度减到0,那么这个石柱将消失,其他蜥蜴就不能经过这个石柱了。蜥蜴一次能够跳跃的最大距离为 d d d,即它只能够跳跃到距离它所在石柱 ≤ d \le d d的石柱或矩阵外。求最少会有几个蜥蜴无法从石柱上跳到矩阵外?

数据范围: 1 ≤ n , m ≤ 20 1\le n,m \le 20 1n,m20 1 ≤ d ≤ 4 1 \le d \le 4 1d4 1 ≤ h ≤ 3 1 \le h \le 3 1h3

解题思路

这个问题应该是一个比较经典的建图问题了。其过程就是将石柱转化为图中的点,然后连边进行求解。容易想到用一个超级源点连接所有蜥蜴所在的石柱,这样就变成了单源最大流问题。然后遍历所有矩阵中的石柱,若这个石柱距离矩阵边界 ≤ d \le d d,那么就连一条边。但其中有一个问题:相邻的点之间如何连边?肯定不能直接连两对边,否则最大流会计算错误。那么到底应该如何连边呢?

这里我们想到:如果将一个点拆分成一个入点和一个出点,那么就可以正常建图了。我们采用如下的思想:将每一个点的出点连接一条流量为inf的有向边到相邻点的入点,而每个结点的入点连一条流量为自身石柱高度的有向边到自己的出点,这样就可以解决问题了。同时,我们需要从超级源点分别连接一条流量为1的边到初始有蜥蜴的点的入点,再对每一个能够跳到矩阵外的点连接一条流量为inf的出边到汇点。建完图后在图上跑Dinic算法即可得到答案。时间复杂度不好分析,但大约是 O ( n 3 m 3 ) O(n^3m^3) O(n3m3)

AC代码

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
inline ll exgcd(ll a, ll b, ll& x, ll& y) {if (b == 0) {x=1; y=0; return a;} ll d = exgcd(b, a%b, y, x); y -= a/b*x; return d;}
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

const int N = 405;
char str[N][N];
char xi[N][N];
int n,m,d;

const int M = 5e4;
const int INF = 1e9;

int cnt = 1, head[N*2];  // 边计数,必须从1开始计数!否则算法会出错
struct {int to, next;ll w;} e[M*2];  // 链式前向星
void addedge(int u, int v, ll w) {
    cnt++;
    e[cnt].to = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}
int now[N*2], dep[N*2];  // 记录所在的层次(深度)
bool bfs(int s, int t) {  // 构造分层图
    fori(1, n) dep[i] = INF;
    dep[s] = 0;  // 从起点s开始分层
    now[s] = head[s];  // 当前弧优化,now是head的副本
    queue<int> que;
    que.push(s);
    while (que.size()) {
        int u = que.front();
        que.pop();
        for (int i=head[u];i>0;i=e[i].next) {  // 搜索点u的邻居,邻居位于下一层
            int v = e[i].to;
            if (e[i].w>0&&dep[v] == INF) {  // 代表还有容量
                que.push(v);
                now[v] = head[v];
                dep[v] = dep[u]+1;
                if (v == t) return true;  // 搜索到终点,返回true
            }

        }
    }
    return false;
}

ll dfs(int u, int t, ll sum) {
    if (u == t) return sum;
    ll k, flow = 0;
    for (int i=now[u];i>0&&sum>0;i=e[i].next) {
        now[u] = i;
        int v = e[i].to;
        if (e[i].w>0&&dep[v] == dep[u]+1) {  // 分层,用dep限制只能访问下一层
            k = dfs(v, t, min(sum, e[i].w));
            if (k == 0) dep[v] = INF;
            e[i].w -= k;
            e[i^1].w += k;
            flow += k;  // flow表示经过该点的所有流量总和
            sum -= k;  // sum表示经过该点的剩余流量
        }
    }
    return flow;  // 返回流量大小
}

ll dinic(int s, int t) {
    ll ans = 0;
    while (bfs(s, t)) ans += dfs(s, t, INF);  // 先后做bfs和dfs,当t不在残留网络中时退出
    return ans;
}

void solve() {
    cin >> n >> m >> d;
    fori(1, n) cin >> str[i]+1;
    fori(1, n) cin >> xi[i]+1;

    // 1超级源点 2汇点

    vector<pii> vec;
    fori(1, n) forj(1, m) {
        if (str[i][j] == '0') continue;
        vec.push_back({i, j});
    }

    int nn = 2+2*vec.size();
    int se = 0;
    fori(0, vec.size()-1) {
        int x = vec[i].first, y = vec[i].second;
        int u = 3+i;
        if (x<=d||y<=d||n+1-x<=d||m+1-y<=d) {
            addedge(u+vec.size(), 2, INF);
            addedge(2, u+vec.size(), 0);
        }
        if (xi[x][y] == 'L') {
            se++;
            addedge(1, u, 1);
            addedge(u, 1, 0);
        }
        addedge(u, u+vec.size(), str[x][y]-'0');
        addedge(u+vec.size(), u, 0);
        forj(0, vec.size()-1) {
            if (i == j) continue;
            int v = 3+j;
            double dis = sqrt((vec[j].first-x)*(vec[j].first-x)+(vec[j].second-y)*(vec[j].second-y));
            if (dis>d) continue;
            addedge(u+vec.size(), v, INF);
            addedge(v, u+vec.size(), 0);
        }
    }
    n = nn;

    int ans = dinic(1, 2);
    cout << se-ans << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

洛谷 P2071 座位安排

题目大意

车上有 n n n排座位,每排有2个座位。刚好有 2 n 2n 2n个人要乘车,每个人都有两个自己意向的座位排号,如1 2表示这个人想做第一排或者第二排。求最大能够满足的乘客数量。

数据范围: n ≤ 2000 n \le 2000 n2000

解题思路

这题也是一个比较经典的二分图匹配问题,可以采用最大流的方式求解。接下来我们来看看如何建图。直接建图发现不太好设计连接各个节点的边,因此考虑拆点建图。把每个座位拆成一个入点和一个出点,其中入点用于连接源点,出点用于连接汇点。但这个时候先不要着急连接入点和源点,先用一条容量为2的边连接各个点的入点和出点,因为一排有2个座位。然后每个出点都连接一条容量为inf的边到汇点。

接下来就需要连接源点和入点了。对于输入的每个人,根据他们的偏好,假设输入的两个座位排号为 u u u v v v,那么我们直接将源点和 u u u入点用一条容量为1的边连接起来,再将 u u u的入点和 v v v的入点用一条容量为1的边连接起来,即可完成建图。

P2071

在建好的图上跑Dinic算法即可得到答案。由于在二分图上跑最大流的时间复杂度约为 O ( n m ) O(\sqrt{n} m) O(n m),本题的时间复杂度约为 O ( n 3 2 ) O(n^\frac{3}{2}) O(n23)

AC代码

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}
int n;
const int N = 4e3+7;
const int M = 4e4+7;
const int INF = 1e9;

int cnt = 1, head[N];  // 边计数,必须从1开始计数!否则算法会出错
struct {int to, next;ll w;} e[M];  // 链式前向星
void addedge(int u, int v, ll w) {
    cnt++;
    e[cnt].to = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}
int now[N], dep[N];  // 记录所在的层次(深度)
bool bfs(int s, int t) {  // 构造分层图
    fori(1, n) dep[i] = INF;
    dep[s] = 0;  // 从起点s开始分层
    now[s] = head[s];  // 当前弧优化,now是head的副本
    queue<int> que;
    que.push(s);
    while (que.size()) {
        int u = que.front();
        que.pop();
        for (int i=head[u];i>0;i=e[i].next) {  // 搜索点u的邻居,邻居位于下一层
            int v = e[i].to;
            if (e[i].w>0&&dep[v] == INF) {  // 代表还有容量
                que.push(v);
                now[v] = head[v];
                dep[v] = dep[u]+1;
                if (v == t) return true;  // 搜索到终点,返回true
            }

        }
    }
    return false;
}

ll dfs(int u, int t, ll sum) {
    if (u == t) return sum;
    ll k, flow = 0;
    for (int i=now[u];i>0&&sum>0;i=e[i].next) {
        now[u] = i;
        int v = e[i].to;
        if (e[i].w>0&&dep[v] == dep[u]+1) {  // 分层,用dep限制只能访问下一层
            k = dfs(v, t, min(sum, e[i].w));
            if (k == 0) dep[v] = INF;
            e[i].w -= k;
            e[i^1].w += k;
            flow += k;  // flow表示经过该点的所有流量总和
            sum -= k;  // sum表示经过该点的剩余流量
        }
    }
    return flow;  // 返回流量大小
}

ll dinic(int s, int t) {
    ll ans = 0;
    while (bfs(s, t)) ans += dfs(s, t, INF);  // 先后做bfs和dfs,当t不在残留网络中时退出
    return ans;
}

void solve() {
	int nn;
    cin >> nn;
    n = 2*nn+1;

    fori(1, 2*nn) {
        int u, v;
        cin >> u >> v;
        addedge(0, u, 1);
        addedge(u, 0, 0);
        addedge(u, v, 1);
        addedge(v, u, 0);
    }

    fori(1, nn) {
        addedge(i, i+nn, 2);
        addedge(i+nn, i, 0);
        addedge(i+nn, n, INF);
        addedge(n, i+nn, 0);
    }

    cout << dinic(0, n) << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

洛谷 P1344 [USACO4.4] 追查坏牛奶 Pollutant Control

题目大意

给你一张 n n n个节点 m m m条有向边的网络,每条边有一个权值 w w w,代表这条边的流量,求这个网络的最小割,以及满足最小割情况下的最少边数。

数据范围: 2 ≤ n ≤ 32 2 \le n \le 32 2n32 0 ≤ m ≤ 1 0 3 0 \le m \le10^3 0m103 0 ≤ w ≤ 2 × 1 0 6 0 \le w \le 2\times 10^6 0w2×106

解题思路

非常经典的最小割题目,只不过这道题需要额外求出满足最小割的最少边数。那么,我们首跑最大流求出最小割,然后在最小割的基础上将所有没有满流的边容量设为inf,再将满流的边容量设为1,再跑一遍最大流即可得到最小割边数量(摘自OI-Wiki)。时间复杂度约为 O ( n 2 m ) O(n^2m) O(n2m)

AC代码

判断一条边是否满流,只需要记录下这一条边的编号,跑完Dinic后判断这条边的权值是否变为0即可。

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

int n;
const int N = 205;
const int M = 2e4;
const int INF = 1e9;

int cnt = 1, head[N];  // 边计数,必须从1开始计数!否则算法会出错
struct {int to, next;ll w;} e[M];  // 链式前向星
void addedge(int u, int v, ll w) {
    cnt++;
    e[cnt].to = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}
int now[N], dep[N];  // 记录所在的层次(深度)
bool bfs(int s, int t) {  // 构造分层图
    fori(1, n) dep[i] = INF;
    dep[s] = 0;  // 从起点s开始分层
    now[s] = head[s];  // 当前弧优化,now是head的副本
    queue<int> que;
    que.push(s);
    while (que.size()) {
        int u = que.front();
        que.pop();
        for (int i=head[u];i>0;i=e[i].next) {  // 搜索点u的邻居,邻居位于下一层
            int v = e[i].to;
            if (e[i].w>0&&dep[v] == INF) {  // 代表还有容量
                que.push(v);
                now[v] = head[v];
                dep[v] = dep[u]+1;
                if (v == t) return true;  // 搜索到终点,返回true
            }

        }
    }
    return false;
}

ll dfs(int u, int t, ll sum) {
    if (u == t) return sum;
    ll k, flow = 0;
    for (int i=now[u];i>0&&sum>0;i=e[i].next) {
        now[u] = i;
        int v = e[i].to;
        if (e[i].w>0&&dep[v] == dep[u]+1) {  // 分层,用dep限制只能访问下一层
            k = dfs(v, t, min(sum, e[i].w));
            if (k == 0) dep[v] = INF;
            e[i].w -= k;
            e[i^1].w += k;
            flow += k;  // flow表示经过该点的所有流量总和
            sum -= k;  // sum表示经过该点的剩余流量
        }
    }
    return flow;  // 返回流量大小
}

ll dinic(int s, int t) {
    ll ans = 0;
    while (bfs(s, t)) ans += dfs(s, t, INF);  // 先后做bfs和dfs,当t不在残留网络中时退出
    return ans;
}

vector<int> edges;
void solve() {
	int m;
    cin >> n >> m;
    // s = 1, e = n
    fori(1, m) {
        int u,v,w;
        cin >> u >> v >> w;
        addedge(u, v, w);
        edges.push_back(cnt);
        addedge(v, u, 0);
    }

    ll ans = dinic(1, n);
    for (auto ee: edges) {
        if (e[ee].w == 0) e[ee].w = 1;
        else e[ee].w = INF;
        e[ee^1].w = 0;  // 记得把反向边权值设回0哦
    }
    ll cc = dinic(1, n);
    cout << ans << " " << cc << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

洛谷 P1345 [USACO5.4] 奶牛的电信Telecowmunication

题目大意

给你一个 n n n个节点 m m m条边的无向图,和一对节点 s s s t t t,求出让 s s s t t t不连通最少需要删除的节点数量。不可以直接删除 s s s t t t,且保证给出的图 s s s t t t连通。

数据范围: 1 ≤ n ≤ 100 1\le n \le 100 1n100 1 ≤ m ≤ 600 1 \le m \le 600 1m600

解题思路

还是考虑拆点建图。每一个节点的入点连一条边权为1的有向边到其出点。对于每一条给定的边 ( u , v ) (u,v) (u,v),直接从 u u u的入点连一条权值为inf的边到 v v v的出点,也从 v v v的入点连一条权值为inf的边到 u u u的出点。最后 s s s的出点为源点, t t t的入点为汇点跑Dinic最大流即可求解出答案。

AC代码

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

int n;
const int N = 205;
const int M = 2e4;
const int INF = 1e9;

int cnt = 1, head[N];  // 边计数,必须从1开始计数!否则算法会出错
struct {int to, next;ll w;} e[M];  // 链式前向星
void addedge(int u, int v, ll w) {
    cnt++;
    e[cnt].to = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}
int now[N], dep[N];  // 记录所在的层次(深度)
bool bfs(int s, int t) {  // 构造分层图
    fori(1, n) dep[i] = INF;
    dep[s] = 0;  // 从起点s开始分层
    now[s] = head[s];  // 当前弧优化,now是head的副本
    queue<int> que;
    que.push(s);
    while (que.size()) {
        int u = que.front();
        que.pop();
        for (int i=head[u];i>0;i=e[i].next) {  // 搜索点u的邻居,邻居位于下一层
            int v = e[i].to;
            if (e[i].w>0&&dep[v] == INF) {  // 代表还有容量
                que.push(v);
                now[v] = head[v];
                dep[v] = dep[u]+1;
                if (v == t) return true;  // 搜索到终点,返回true
            }

        }
    }
    return false;
}

ll dfs(int u, int t, ll sum) {
    if (u == t) return sum;
    ll k, flow = 0;
    for (int i=now[u];i>0&&sum>0;i=e[i].next) {
        now[u] = i;
        int v = e[i].to;
        if (e[i].w>0&&dep[v] == dep[u]+1) {  // 分层,用dep限制只能访问下一层
            k = dfs(v, t, min(sum, e[i].w));
            if (k == 0) dep[v] = INF;
            e[i].w -= k;
            e[i^1].w += k;
            flow += k;  // flow表示经过该点的所有流量总和
            sum -= k;  // sum表示经过该点的剩余流量
        }
    }
    return flow;  // 返回流量大小
}

ll dinic(int s, int t) {
    ll ans = 0;
    while (bfs(s, t)) ans += dfs(s, t, INF);  // 先后做bfs和dfs,当t不在残留网络中时退出
    return ans;
}

void solve() {
	int nn,m,s,t;
    cin >> nn >> m >> s >> t;
    n = 2*nn;
    fori(1, m) {
        int u,v;
        cin >> u >> v;
        addedge(u+nn, v, INF);
        addedge(v, u+nn, 0);
        addedge(v+nn, u, INF);
        addedge(u, v+nn, 0);
    }
    fori(1, nn) {
        addedge(i, i+nn, 1);
        addedge(i+nn, i, 0);
    }

    ll ans = dinic(s+nn, t);
    cout << ans << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

HDU 7488 猫咪们狂欢

题目大意

n n n只猫和两棵节点数为 n n n的树,树的每条边都有一个权值 w w w,被称为“狂欢值”。每只猫都对应了树上的一个编号,即第 i i i只猫对应了两棵树上的第 i i i个节点,每只猫晚上只能选择在这两棵树上的其中一棵的对应节点上睡觉。但这些猫当中有 k k k只是“狂欢猫”,它们晚上不会睡觉,且如果相邻节点上也有“狂欢猫”,那么这棵树的狂欢值就会增加 w u , v w_{u,v} wu,v,即连接这两个节点的权值。请你安排这 n n n只猫分别位于哪棵树上,以最大化两棵树的总狂欢值。

数据范围:有多组测试样例,组数 1 ≤ T ≤ 50 1\le T \le 50 1T50,对每组测试样例 1 ≤ k ≤ n ≤ 1 0 3 1\le k \le n \le 10^3 1kn103 1 ≤ w ≤ 20 1 \le w \le 20 1w20;对所有测试样例保证 ∑ n ≤ 1 0 4 \sum n \le 10^4 n104

解题思路

这道题来源于2024杭电多校第5场。这道题和洛谷P1361很像,是一个经典的最小割问题。这道题的建图非常巧妙:对于每只猫,将其看作网络中的一个节点 u u u,我们用一条有向边连接源点和 u u u,代表将猫 u u u安排在树1中;再用一条有向边连接 u u u和汇点,代表将猫 u u u安排在树2中。因为猫必然不可能同时在两棵树上,因此在该网络中跑最大流时必定有至少一条边满流,把这条边割掉即可得到权值更大的那条边。但在此题中,由于单纯只考虑把一只猫安排在哪棵树上不会产生收益,因此这两条边的容量都为0,可以不连。

接下来考虑多只猫组合的情况:一棵树上一条边连接了两个节点,代表这两只猫如果同时存在就会增加收益。如何在我们构建的网络中表示出这个收益呢?这里我们先考虑树1中的某条边 ( u , v ) (u, v) (u,v)在网络中引出一个新的节点 x x x,从源点连接一条有向边到 x x x,容量为树上这条边的权值,再从 x x x分别连两条有向边到 u u u v v v,容量为inf。而对于树2上的某条边 ( u , v ) (u, v) (u,v)在网络中再引出一个新的节点 y y y,从 u u u v v v分别连两条有向边到 y y y,容量为inf,从 y y y连接一条有向边到汇点,容量为树上这条边的权值。这样我们就构建出了一个如下图所示的网络。
7488
这样,对树1和2的每一条边都新建节点并且按照上述方式连边,即可得到一张网络。对该网络跑Dinic求出最大流(最小割),答案即为两棵树上的所有边权之和-最小割。时间复杂度约为 O ( n 2 ) O(n^2) O(n2)。对上述方法的证明可以参见OI-Wiki

AC代码

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

int n;
const int N = 3e3+7;
const int M = 5e6+7;
const int INF = 1e9;

int cnt = 1, head[N];  // 边计数,必须从1开始计数!否则算法会出错
struct {int to, next;ll w;} e[M];  // 链式前向星
void adde(int u, int v, ll w) {
    cnt++;
    e[cnt].to = v;
    e[cnt].w = w;
    e[cnt].next = head[u];
    head[u] = cnt;
}
void addedge(int u, int v, ll w) {
    adde(u, v, w);
    adde(v, u, 0);
}
int now[N], dep[N];  // 记录所在的层次(深度)
bool bfs(int s, int t) {  // 构造分层图
    fori(1, n) dep[i] = INF;
    dep[s] = 0;  // 从起点s开始分层
    now[s] = head[s];  // 当前弧优化,now是head的副本
    queue<int> que;
    que.push(s);
    while (que.size()) {
        int u = que.front();
        que.pop();
        for (int i=head[u];i>0;i=e[i].next) {  // 搜索点u的邻居,邻居位于下一层
            int v = e[i].to;
            if (e[i].w>0&&dep[v] == INF) {  // 代表还有容量
                que.push(v);
                now[v] = head[v];
                dep[v] = dep[u]+1;
                if (v == t) return true;  // 搜索到终点,返回true
            }

        }
    }
    return false;
}

ll dfs(int u, int t, ll sum) {
    if (u == t) return sum;
    ll k, flow = 0;
    for (int i=now[u];i>0&&sum>0;i=e[i].next) {
        now[u] = i;
        int v = e[i].to;
        if (e[i].w>0&&dep[v] == dep[u]+1) {  // 分层,用dep限制只能访问下一层
            k = dfs(v, t, min(sum, e[i].w));
            if (k == 0) dep[v] = INF;
            e[i].w -= k;
            e[i^1].w += k;
            flow += k;  // flow表示经过该点的所有流量总和
            sum -= k;  // sum表示经过该点的剩余流量
        }
    }
    return flow;  // 返回流量大小
}

ll dinic(int s, int t) {
    ll ans = 0;
    while (bfs(s, t)) ans += dfs(s, t, INF);  // 先后做bfs和dfs,当t不在残留网络中时退出
    return ans;
}

void solve() {
	int nn,m;
    cin >> nn >> m;
    // init

    n = 3*nn-1;
    cnt = 1;
    fori(0, n) head[i] = 0;

    set<int> sets;
    fori(1, m) {
        int x;
        cin >> x;
        sets.insert(x);  // 狂欢猫
    }

    ll ans = 0;
    fori(1, nn-1) {
        int u,v,w;
        cin >> u >> v >> w;
        if (sets.find(u) == sets.end()||sets.find(v) == sets.end()) continue;
        ans += w;
        addedge(0, nn+i, w);

        addedge(nn+i, u, INF);
        addedge(nn+i, v, INF);
    }

    fori(1, nn-1) {
        int u,v,w;
        cin >> u >> v >> w;
        if (sets.find(u) == sets.end()||sets.find(v) == sets.end()) continue;
        ans += w;
        addedge(2*nn+i-1, n, w);

        addedge(u, 2*nn+i-1, INF);
        addedge(v, 2*nn+i-1, INF);
    }

    cout << (ans-dinic(0, n)) << endl;

}

signed main() {
	IOS;
	int t;
	cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}

洛谷 P2053 [SCOI2007] 修车

题目大意

n n n个顾客和 m m m位修车人员,每个修车人员对不同的车都有不同的修车时间。给定一个 n × m n\times m n×m的矩阵 T T T T i j T_{ij} Tij代表第 i i i辆车被第 j j j个维修员维修所需的时间,假设 n n n辆车同时到达,计算每辆车从开始等待到维修完毕的最短平均等待时间。

数据范围: 2 ≤ M ≤ 9 2\le M \le 9 2M9 1 ≤ N ≤ 60 1 \le N \le 60 1N60 1 ≤ T i j ≤ 1 0 3 1 \le T_{ij} \le 10^3 1Tij103

解题思路

这道题是一道经典的建图求费用流的题目。如何辨别求的是最大流还是费用流?其实也说不好。一般来说,如果题目只要求最大匹配数/最大权值等,大概率是最大流或者最小割问题。而最短路径,最少时间,最少花费等问题则大概是率费用流问题。最重要的还是看题目给的条件:一条边上会包含一个还是两个信息?如果是一个信息那么很可能就是最大流,如果有两个或以上的信息那么就应该是费用流了。

回到这道题,如何建图?其实这道题涉及到了一个非常巧妙的点:由于每个维修师傅修不同的车时间是不一样的,因此我们需要将 m m m个师傅拆成 n × m n\times m n×m个点,代表师傅修完几台车后来修这一台车。那么如何保证同一时刻一个师傅只修一辆车呢?这就需要借助最大流的限制了。容易想到,将 n n n辆车连一条容量为1的边到源点/汇点,就可以确保这 n n n台车都被维修了。那么其实图就连出来了,从源点分别连 n × m n\times m n×m条容量为1的边到 m × n m \times n m×n个代表师傅的点,再将这 n × m n\times m n×m个点与 n n n台车完全二分匹配,每条边的容量为1,然后 n n n辆车连汇点即可。

关键是每条边的花费如何确定?这里我们需要转化思维。计算每辆车的等待时间之和每辆车的被等待时间之和是等价的。例如,一个师傅按顺序修车1,2,3,那么直接计算等待时间之和为 T 1 + T 1 + T 2 + T 1 + T 2 + T 3 = 3 T 1 + 2 T 2 + T 3 T_1+T_1+T_2+T_1+T_2+T_3=3T_1+2T_2+T_3 T1+T1+T2+T1+T2+T3=3T1+2T2+T3。实际上就是第一辆车的被等待时间为 3 T 1 3T_1 3T1,第二辆车为 2 T 2 2T_2 2T2,第三辆车为 T 3 T_3 T3。因此,每一个 n × m n\times m n×m个点与 n n n台车之间的连线都有 n n n条边,这其中每一条边的花费为 i T 1 j iT_{1j} iT1j i T 2 j iT_{2j} iT2j i T 3 j iT_{3j} iT3j … \dots i T n j iT_{nj} iTnj,其中 i , j i,j i,j代表师傅 j j j修的第 i i i台车。这里的描述可能有些抽象,但是有一点需要注意的是,不能从每个点 n × m n\times m n×m的点都分别连 n n n条边到某一辆车,因为这样不能够保证一个师傅同一时间内只修一台车。

最后跑最小费用最大流,结果除以 n n n即为答案。

PS:这道题我用我最初的spfa-Dinic费用流一直T on test 2,换了一个spfa-EK就AC了,有时候还是得考虑换个板子(

AC代码

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define pb push_back

#define Yes cout << "Yes\n"
#define No cout << "No\n"
#define YES cout << "YES\n"
#define NO cout << "NO\n"
#define ff first
#define ss second
#define fori(x,y) for(int i=x;i<=(int)(y);++i)
#define forj(x,y) for(int j=x;j<=(int)(y);++j)
#define fork(x,y) for(int k=x;k<=(int)(y);++k)

#define debug(x) cout << #x << " = " << x << endl

typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

ll MOD = 998244353;
ll qpow(ll a,ll p) {ll res=1; while(p) {if (p&1) {res=res*a%MOD;} a=a*a%MOD; p>>=1;} return res;}

/**
 * 最小费用最大流MCMF-spfa-EK版
 * 0/1-indexed
 * 老样子,先加边,再调用mcmf求结果
 */
struct MCMF {
    int n;
    const int INF = 1e9;
    int cnt = -1;  // 边计数,必须从-1开始计数!否则算法会出错
    vector<int> head;  // 链式前向星
    struct node {
        int to, next;
        ll w,cost;
        node(int to, int next, ll w, ll cost): to(to), next(next), w(w), cost(cost) {}
    };  // 链式前向星
    vector<node> e;  // 用于存边的
    vector<ll> dis, flow;
    vector<bool> vis;
    vector<int> pre, last;
    queue<int> que;

    MCMF(int n): n(n) {
        head = vector<int>(n+1, -1);
        e.reserve(n*50);
        dis = flow = vector<ll>(n+1, 0);
        pre = last = vector<int>(n+1, 0);
        que = queue<int>();
        vis = vector<bool>(n+1, false);
    }

    void adde(int u, int v, ll w, ll cost) {
        cnt++;
        e.emplace_back(v, head[u], w, cost);
        head[u] = cnt;
    }
    void addedge(int u, int v, ll w, ll cost) {
        adde(u, v, w, cost);
        adde(v, u, 0, -cost);
    }

    bool spfa(int s,int t) {
        fori(0, n) {
            dis[i] = flow[i] = INF;
            vis[i] = false;
        }
        que.push(s); vis[s] = 1; dis[s] = 0; pre[t] = -1;
        
        while (que.size()) {
            int now = que.front();
            que.pop();
            vis[now] = 0;
            for (int i=head[now]; ~i; i=e[i].next) {
                if (e[i].w>0 && dis[e[i].to]>dis[now]+e[i].cost) {
                    dis[e[i].to] = dis[now]+e[i].cost;
                    pre[e[i].to] = now;
                    last[e[i].to] = i;
                    flow[e[i].to] = min(flow[now],e[i].w);
                    if (!vis[e[i].to]) {
                        vis[e[i].to] = 1;
                        que.push(e[i].to);
                    }
                }
            }
        }
        return pre[t] != -1;
    }

    void mcmf(int s, int t, ll& mf, ll& mc) {
        mf = mc = 0;
        while (spfa(s,t)) {
            int now = t;
            mf += flow[t];
            mc += flow[t]*dis[t];
            while (now!=s) {  //从源点一直回溯到汇点 
                e[last[now]].w -= flow[t];  //flow和dis容易搞混 
                e[last[now]^1].w += flow[t];
                now = pre[now];
            }
        }
    }
};

const int NN = 505;

int arr[NN][NN];
void solve() {
    int n,m;
    cin >> m >> n;
    fori(1, n) forj(1, m) cin >> arr[i][j];

    int s = 0, t = n*(m+1)+1;
    MCMF mcmf(t);
    fori(1, n) forj(1, m) {
        int x = arr[i][j];
        fork(1, n) {
            mcmf.addedge(i, n*j+k, 1, x*k);  // 最重要的地方!
        }
        mcmf.addedge(n*j+i, t, 1, 0);  // 汇点
    }
    fori(1, n) mcmf.addedge(s, i, 1, 0);  // 源点

    ll mf, mc; mcmf.mcmf(s, t, mf, mc);
    cout << fixed << setprecision(2) << 1.0*mc/n << endl;
}

signed main() {
	IOS;
	int t = 1;
	while (t--) {
		solve();
	}
	return 0;
}

未完待续

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值