文章目录
前言
再开个坑,这几天打算看看网络流,应该会以洛谷的24题为主,毕竟之前没怎么写过网络流。
P3254 圆桌问题
链接
https://www.luogu.com.cn/problem/P3254
这道基本算裸题了。
题意
输入 #1
4 5
4 5 3 5
3 5 2 6 4
输出 #1
1
1 2 4 5
1 2 3 4 5
2 4 5
1 2 3 4 5
解析
每个单位连所有餐桌,边权为 1
源点连单位, 边权为 r i r_i ri
餐桌连汇点,边权为 c i c_i ci
如图:
至于统计答案,只需要判断单位–桌子之间的边flow是不是 1 就行了
参考代码
#include<bits/stdc++.h>
using namespace std;
const int N = 333,M = 333;
const int maxn = N*M;
const int inf = 0x3f3f3f3f;
struct E{
int to;
int nxt;
int flow;
int cap;
}e[maxn<<1];
int head[maxn],tot=1;
void add_edge(int u,int v,int w){
e[++tot].to = v;
e[tot].cap = w;
e[tot].flow = 0;
e[tot].nxt = head[u];
head[u] = tot;
}
int Q[maxn];
int dep[maxn],cur[maxn],sta[maxn];
bool bfs(int s,int t,int n){
int front = 0,tail = 0;
memset(dep,-1,sizeof dep);
dep[s] = 0;
Q[tail++] = s;
while(front<tail){
int u = Q[front++];
for(int i=head[u];i!=-1;i=e[i].nxt){
int v = e[i].to;
if(e[i].cap>e[i].flow&&dep[v]==-1){
dep[v] = dep[u]+1;
if(v==t) return true;
Q[tail++] = v;
}
}
}
return false;
}
int dinic(int s,int t,int n){
int maxflow = 0;
while(bfs(s,t,n)){
for(int i=0;i<n;i++) cur[i] = head[i];
int u = s,tail = 0;
while (cur[s]!=-1){
if(u==t){
int tp = inf;
for(int i=tail-1;i>=0;i--)
tp = min(tp,e[sta[i]].cap-e[sta[i]].flow);
maxflow += tp;
for(int i=tail-1;i>=0;i--){
e[sta[i]].flow += tp;
e[sta[i]^1].flow -= tp;
if(e[sta[i]].cap-e[sta[i]].flow==0) tail = i;
}
u = e[sta[tail]^1].to;
}
else if(cur[u]!=-1&&e[cur[u]].cap>e[cur[u]].flow&&dep[u]+1==dep[e[cur[u]].to]){
sta[tail++] = cur[u];
u = e[cur[u]].to;
}
else{
while(u!=s&&cur[u]==-1)
u = e[sta[--tail]^1].to;
cur[u] = e[cur[u]].nxt;
}
}
}
return maxflow;
}
int s,t,n,m;
vector<int> a,b;
int main(){
ios::sync_with_stdio(false);
cin>>n>>m;
memset(head,-1,sizeof head);
int x;
int sum = 0;
for(int i=1;i<=n;i++){
cin>>x;
sum+=x;
a.push_back(x);
}
for(int j=1;j<=m;j++){
cin>>x;
b.push_back(x);
}
s = 0,t = n+m+1;
for(int i=0;i<a.size();i++){
add_edge(s,i+1,a[i]);
add_edge(i+1,s,0);
}
for(int i=0;i<a.size();i++){
for(int j=0;j<b.size();j++){
int u = i+1,v = a.size()+j+1;
add_edge(u,v,1);
add_edge(v,u,0);
}
}
for(int i=0;i<b.size();i++){
int u = a.size()+i+1;
add_edge(u,t,b[i]);
add_edge(t,u,0);
}
int ans = dinic(s,t,t);
if(ans==sum){
cout<<1<<endl;
for(int u=1;u<=n;u++){
for(int i=head[u];i!=-1;i=e[i].nxt){
int v = e[i].to;
if(v==s) continue;
if(e[i].flow) cout<<v-n<<" "; //统计答案
}
cout<<endl;
}
}
else{
cout<<0<<endl;
}
}
P2762 太空飞行计划问题(最小割)
题目链接
https://www.luogu.com.cn/problem/P2762
感觉算半个结论题,虽然并不难猜…
题目大意
输入 #1
2 3
10 1 2
25 2 3
5 6 7
输出 #1
1 2
1 2 3
17
思路
最大权闭合子图,最小割的一个应用。
yysy,我也不知道怎么严谨证明…
引用洛谷评论区大佬的话,“跑最小割相当于选择部分的实验和部分的仪器,剩下的实验和仪器就会被割掉,此时再用实验的总价值减去可能得到的最大值,即为其所要求的答案” 。
我是这样想的:
实验连源点,仪器连汇点,中间设inf ,如下图。(画图时脑子抽了,图里的项目应该是仪器2333)
我们假设实验全都选,看看至少需要多少钱的仪器,作差就是答案。
简单证明一下:
设实验的全集是U , 有一个不被选择的子集V 。
那么必然存在仪器的某个子集 K仅存在于集合V 中并且有 v a l u e ( K ) < v a l u e ( V ) value(K)<value(V) value(K)<value(V) 。
此时,因为从子集K中流出的流量仅从V 流入 , 此时从V流出的最大流最多只有 v a l u e ( K ) value(K) value(K)
做差后就抵消掉了。
(好吧,只能意会一下,具体见证明:https://blog.csdn.net/can919/article/details/77603353)
至于输出路径
对于每一个 c i c_i ci,把它删掉以后再跑一遍最小割,看看和原来的结果是否相差 c i c_i ci,如果是则证明该边必选,然后再根据选的仪器就可以反推回应该选哪些实验。
至于恶心的读入
写这题的时候刚从题解区大佬那里学到了一个神技。(本题最大收获23333333)
一行中输入的整数个数不确定怎么办(读到换行符结束本行)?
通常的处理方法是 getline(cin,string) 获得一整行,然后手动从string里把每个整数拆出来,或者用stringssream—但其实也很麻烦。
然而快读其实是可以处理这个问题的:
int read(){
char c;int r=0;
while (c<'0' || c>'9') c=getchar();
while (c>='0' && c<='9')
{
r=r*10+c-'0';
c=getchar();
}
if (c=='\n') flg= true; // 如果读到'\n' , 打上标记
return r;
}
flg = false; // 一直读,直到读到换行符
while (!flg){
int x;
x = read();
ll[i].push_back(x);
int u = i,v = x+n;
maze[u][v] = inf;
maze[v][u] = 0;
}
代码
本来最大流最小割一直用 Dinic 做的,写这题的时候翻板子发现 邻接矩阵实现的SAP 代码比较短,貌似可读性也不错。
所以这题拿 SAP 算法写的 ,然而并不怎么了解板子, 过两天找个大佬学学 。
#include<bits/stdc++.h>
using namespace std;
const int N = 1100;
int maze[N][N];
int gap[N],dis[N],pre[N],cur[N];
const int inf = 0x3f3f3f3f;
int flow[N][N];
bool flg;
int read(){
char c;int r=0;
while (c<'0' || c>'9') c=getchar();
while (c>='0' && c<='9')
{
r=r*10+c-'0';
c=getchar();
}
if (c=='\n') flg= true; // 如果读到'\n' , 打上标记
return r;
}
int sap(int start,int end,int nodenum){
memset(cur,0,sizeof cur);
memset(dis,0,sizeof dis);
memset(gap,0,sizeof gap);
memset(flow,0,sizeof flow);
int u = pre[start] = start,maxflow = 0,aug = -1;
gap[0] = nodenum;
while(dis[start]<nodenum){
loop:
for(int v=cur[u];v<nodenum;v++) {
if (maze[u][v] - flow[u][v] && dis[u] == dis[v] + 1) {
if (aug == -1 || aug > maze[u][v] - flow[u][v])
aug = maze[u][v] - flow[u][v];
pre[v] = u;
u = cur[u] = v;
if (v == end) {
maxflow += aug;
for (u = pre[u]; v != start; v = u, u = pre[u]) {
flow[u][v] += aug;
flow[v][u] -= aug;
}
aug = -1;
}
goto loop;
}
}
int mindis = nodenum - 1;
for(int v=0;v<nodenum;v++){
if(maze[u][v]-flow[u][v]&&mindis>dis[v]){
cur[u] = v;
mindis = dis[v];
}
}
if((--gap[dis[u]])==0) break;
gap[dis[u]=mindis+1]++;
u = pre[u];
}
return maxflow;
}
int p[N],a[N];
bool vis1[N],vis2[N];
vector<int> ll[N];
int main(){
int n,m;
n = read(),m = read();
int s = 0,t = n+m+1;
int sum = 0;
for(int i=1;i<=n;i++){
p[i] = read();
sum += p[i];
maze[0][i] = p[i];
maze[i][0] = 0;
string str;
flg = false; // 一直读,直到读到换行符
while (!flg){
int x;
x = read();
ll[i].push_back(x);
int u = i,v = x+n;
maze[u][v] = inf;
maze[v][u] = 0;
}
}
for(int i=1;i<=m;i++){
a[i] = read();
int u = i+n,v = t;
maze[u][v] = a[i];
maze[v][u] = 0;
}
int all = sap(s,t,t+1);
int ans = sum - all;
for(int i=1;i<=m;i++){
int u = i+n,v = t;
int save = maze[u][v]; // 删边,跑最小割,与原结果对比
maze[u][v] = 0;
maze[v][u] = 0;
if(all- sap(s,t,t+1)==save){
vis2[i] = true;
}
maze[u][v] = save;
}
for(int i=1;i<=n;i++){
bool ok = true; // 如果一个实验所需的仪器都被选上了,那就选这个实验。
for(auto j:ll[i]){
if(!vis2[j]) ok = false;
}
vis1[i] = ok;
}
for(int i=1;i<=n;i++)
if(vis1[i]) cout<<i<<" ";cout<<endl;
for(int i=1;i<=m;i++)
if(vis2[i]) cout<<i<<" ";cout<<endl;
cout<<ans<<endl;
}
P2774 方格取数问题
很经典的最小割 , 去年某不知名比赛遇到一个改版的,当时当作费用流乱搞没写出来2333
题目
输入 #1
3 3
1 2 3
3 2 3
2 3 1
输出 #1
11
解题思路
网格图或矩阵中相邻点的明显特征就是 i + j i+j i+j 的值奇偶性不同。
题意中相邻格子不取,事实上就构成了一个以奇偶区分二分图,奇数格子在一边,偶数格子在另一边:
一边连源点,边权为该点权值
一边连汇点,边权也是该点权值
中间相邻的边相连,权值为inf(避免被割掉)
如下图,这也是上方给出的样例的练法。
根据最大流 = 最小割 , 我们对此图跑最大流后得到的就是我们想要的:
用最小的权值把图切开
“把图切开”,既没有一条从 S–>T 的路径,也就是保证了选取合法(不选取相邻节点)
我发现这个题,或者说运用最小割解题有个很巧妙的地方就是把那些题意上不能割的边赋值为inf
代 码
#include<bits/stdc++.h>
using namespace std;
const int N = 5050;
int maze[N][N];
int gap[N],dis[N],pre[N],cur[N];
const int inf = 0x3f3f3f3f;
int flow[N][N];
int mp[111][111];
int sap(int start,int end,int nodenum){
memset(cur,0,sizeof cur);
memset(dis,0,sizeof dis);
memset(gap,0,sizeof gap);
memset(flow,0,sizeof flow);
int u = pre[start] = start,maxflow = 0,aug = -1;
gap[0] = nodenum;
while(dis[start]<nodenum){
loop:
for(int v=cur[u];v<nodenum;v++) {
if (maze[u][v] - flow[u][v] && dis[u] == dis[v] + 1) {
if (aug == -1 || aug > maze[u][v] - flow[u][v])
aug = maze[u][v] - flow[u][v];
pre[v] = u;
u = cur[u] = v;
if (v == end) {
maxflow += aug;
for (u = pre[u]; v != start; v = u, u = pre[u]) {
flow[u][v] += aug;
flow[v][u] -= aug;
}
aug = -1;
}
goto loop;
}
}
int mindis = nodenum - 1;
for(int v=0;v<nodenum;v++){
if(maze[u][v]-flow[u][v]&&mindis>dis[v]){
cur[u] = v;
mindis = dis[v];
}
}
if((--gap[dis[u]])==0) break;
gap[dis[u]=mindis+1]++;
u = pre[u];
}
return maxflow;
}
vector<pair<int,int>> a,b;
int n, m;
int cal(pair<int,int> pa){
return (pa.first-1)*m+pa.second; // 把二位坐标映射成一维的
}
int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
int sum = 0;
for (int i = 1; i <= n; i++){
for (int j = 1; j <= m; j++) {
cin >> mp[i][j];
sum += mp[i][j];
if ((i + j) & 1) a.push_back(make_pair(i, j));
else b.push_back(make_pair(i, j));
}
}
int s = 0,t = n*m+1;
for(int i=0;i<a.size();i++){
int u = s,v = cal(a[i]);
maze[u][v] = mp[a[i].first][a[i].second];
maze[v][u] = 0;
for(int j=0;j<b.size();j++){
if(abs(a[i].first-b[j].first)+abs(a[i].second-b[j].second)==1){ //判断是否相邻
u = cal(a[i]),v = cal(b[j]);
maze[u][v] = inf;
maze[v][u] = 0;
}
}
}
for(int i=0;i<b.size();i++){
int u = cal(b[i]),v = t;
maze[u][v] = mp[b[i].first][b[i].second];
maze[v][u] = 0;
}
cout<<sum - sap(s,t,t+1)<<endl;
}
P1251 餐巾计划问题(费用流)
https://www.luogu.com.cn/problem/P1251
题意
输入 #1
3
1 7 5
11 2 2 3 1
输出 #1
134
思路
很巧妙的费用流建模。
首先每一天肯定要拆分成早上(开始),晚上(结束)两个时刻。
早上收集干净毛巾,晚上处理旧毛巾
想的时候一直在思考一个问题,怎么同时把洗过的毛巾送到汇点和送到第二天
一开始想每天早上连晚上 , 然后最后一天早上连汇点 , 容量为 ∑ i = 1 n r i \sum_{i=1}^n r_i ∑i=1nri
后来发现行不通,还是会和旧毛巾冲突到。
经题解区提醒,发现可以直接让源点连旧毛巾到每天晚上:
相当于每天早上“用完新毛巾后直接上交汇点统计,当了晚上源点送出相同数量的旧毛巾”
这与每天“用完 r i r_i ri条新毛巾后变为旧毛巾”是等价的 , 而且避免了上述建模冲突。
具体建模方法
-
源点连每天早上,容量为 r i r_i ri , 费用为 p p p (直接买毛巾 )
-
源点连每天晚上,容量为 r i r_i ri , 费用为 0 0 0 (早上用掉的旧毛巾,如上述)
-
每天早上连汇点,容量为 r i r_i ri , 费用为 0 0 0 (交到汇点统计答案)
-
每天晚上连 该天+快洗天数的的早上,容量为 i n f inf inf , 费用为 f f f (将部分旧毛巾快洗)
-
每天晚上连 该天+慢洗天数的的早上,容量为 i n f inf inf , 费用为 p p p (将部分旧毛巾慢洗)
-
每天晚上连 第二天晚上,容量为 i n f inf inf , 费用为 0 0 0 (将剩下的旧毛巾留到第二天)
关于快慢洗这里,我们只需要连接对应的那一天就可以,不需要连接后续的每一天。因为洗好的毛巾在那之后哪一天用都一样。
大致如下图 , 中间快慢洗部分没有全部画出。
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn= 2e5+100;
struct E{
int to;
int nxt;
int flow;
int cost;
int cap;
}e[maxn<<1];
const int inf = 1e12+7;
int head[maxn],tot;
int pre[maxn],dis[maxn];
bool vis[maxn];
int N;
void init(int n){
N = n;
tot = 0;
memset(head,-1,sizeof head);
}
void add_edge(int u,int v,int cap,int cost){
e[tot].to = v;e[tot].cap = cap;
e[tot].cost = cost;e[tot].flow = 0;
e[tot].nxt = head[u];head[u] = tot++;
e[tot].to = u;e[tot].cap = 0;
e[tot].cost = -cost;e[tot].flow = 0;
e[tot].nxt = head[v];head[v] = tot++;
}
bool spfa(int s,int t){
queue<int> q;
for(int i=0;i<N;i++){
dis[i] = inf;
vis[i] = false;
pre[i] = -1;
}
dis[s] = 0;
vis[s] = true;
q.push(s);
while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = false;
for(int i=head[u];i!=-1;i=e[i].nxt){
int v = e[i].to;
if(e[i].cap>e[i].flow&&dis[v]>dis[u]+e[i].cost){
dis[v] = dis[u]+e[i].cost;
pre[v] = i;
if(!vis[v]){
vis[v] = true;
q.push(v);
}
}
}
}
if(pre[t]==-1) return false;
else return true;
}
int cost_flow(int s,int t,int &cost){
int flow = 0;
cost = 0;
while(spfa(s,t)){
int min = inf;
for(int i=pre[t];i!=-1;i=pre[e[i^1].to]){
if(min>e[i].cap-e[i].flow)
min = e[i].cap - e[i].flow;
}
for(int i=pre[t];i!=-1;i=pre[e[i^1].to]){
e[i].flow += min;
e[i^1].flow -= min;
cost += e[i].cost*min;
}
flow += min;
}
return flow;
}
int a[maxn];
signed main(){
int day;
cin>>day;
for(int i=1;i<=day;i++){
cin>>a[i];
}
int p,m,f,n,s;
cin>>p>>m>>f>>n>>s;
int S = 0,T = 2*day+1;
N = T+1;
init(N);
for(int i=1;i<=day;i++){
int start = i,end = i+day;
add_edge(S,start,a[i],p); // 源点连每天早上,容量为ri , 费用为p (直接买毛巾 )
add_edge(S,end,a[i],0); // 源点连每天晚上,容量为ri , 费用为0 (早上用掉的旧毛巾)
add_edge(start,T,a[i],0); // 每天早上连汇点,容量为ri , 费用为0 (交到汇点统计答案)
if(start+m<=day)
add_edge(end,start+m,inf,f); // (将部分旧毛巾快洗)
if(start+n<=day)
add_edge(end,start+n,inf,s); // (将部分旧毛巾慢洗)
if(i!=day)
add_edge(end,end+1,inf,0); // (将剩下的旧毛巾留到第二天)
}
int cost = 0;
N = T+1;
cost_flow(S,T,cost);
cout<<cost<<endl;
}