“东信杯”广西大学第一届程序设计竞赛(同步赛)F-出装方案(二分图最大匹配/状压dp/最大费用最大流)

题目

思路来源

https://ac.nowcoder.com/acm/contest/view-submission?submissionId=37522548(MCMF)

https://ac.nowcoder.com/acm/contest/view-submission?submissionId=37514575(状压dp)

心得

本来就是一个二分图最大权匹配的KM板子题,上了板子就过了。

但是,看到了一个高中生的状压dp做法,

不得不说,有些高中生真的认真,代码通俗易懂可读性强,

的确是一步一个脚印扎扎实实写的代码(嗯白膜法师这种已经不能叫高中生了)

所以,借着人家的代码学一学从没学过的状压dp

QAQ睡个觉果然读代码能力强了不少,以后看不懂代码要及时休息啊,效率++

 

补一下MCMF的题解

按原来的样子建图,

二分图部分,每条边,费用=强度提升值,流量=1,

从每个左侧顶点流出来的流量就是1,只能匹配一个

超级源点s连左侧顶点,右侧顶点连超级汇点t,这些边,费用=0,流量=1

跑一遍最大费用最大流,费用即为总强度提升的部分

可以边权取负,跑最小费用最大流,最后再把tot的负再取回来

也可以每次spfa增广的时候找最长路,很灵活吖

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
 
int a[110][110];
int dp[1 << 15];
 
int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        int n; scanf("%d", &n);
        for(int i = 0; i < n; i++)
            for(int j = 0; j < n; j++) scanf("%d", &a[i][j]);
        memset(dp, 0, sizeof(dp));
        for(int S = 1; S < (1 << n); S++)
        {
            int tot = 0;
            for(int i = 0; i < n; i++) tot += ((S >> i) & 1);
            tot--;
            for(int i = 0; i < n; i++)
            {
                if((S >> i) & 1)
                    dp[S] = std::max(dp[S], dp[S ^ (1 << i)] + a[tot][i]);
            }
        }
        printf("%d\n", dp[(1 << n) - 1]);
    }
    return 0;
}

个人理解

先把人家的代码粘过来。

状压dp是二进制枚举思想,用第i位来枚举第i件物品,

1代表该物品已被选,不能再选了;0代表该物品尚未被选。

tot是枚举的二进制数中的1的个数,第tot个人可以在尚未被选的里面选一个。

为了与下标对应,tot减1。

核心句

if((S >> i) & 1)dp[S] = std::max(dp[S], dp[S ^ (1 << i)] + a[tot][i]);

感觉这个很形象啊QAQ,

第tot个人知道,当他选第i件商品的时候,能达成tot个人之和的最优,

于是他让前tot-1个人别选第i件商品了,“都让让都让让,你们从剩下tot-1件商品里挑,把第i件给我"。

我写博客咋变成这个画风了

然而我们不知道第tot个人选哪件能达成最优,

所以我们就要在状压的某状态(如01010101)的某个1值里,枚举一件给第tot个人。

 

即n个人分n件商品的最优,一定是(n-1)个人分其中(n-1)个商品的最优,加上最后一个人选剩下一件商品所求得。

状压dp,好就好在当第i位为1时,把第i位变0原值会变小,所以大一定从小转移而来。

因为有这个最优子结构的性质,tot个人的最优,一定是由tot-1个人的某种最优选法转移而来,故有等式成立。

 

如11可以由01或10转移而来,

前一种转移代表第一个人选了第二件而第二个人选了第一件,

后一种转移代表第一个人选了第一件而第二个人选了第二件,

但是既然这两种已经被前两个人选了,我们只用11保留这两种情况的最大值即最优情况。

 

其实这就跟递归一样,你不知道某个状态的值,甚至其子状态的值,

但是最底层的值和转移方程你是知道的,所以你就知道了一切.jpg。

 

状压dp一般很明显,O(2^n)所以一般n不超过15。

当然KM算法O(n^3)在这里显得优了好多。

只是学习一波状压dp罢了。

传说中的dp一题能想一晚上真不是吹的啊

代码2(MaxCostMaxFlow,MCMF)

要学会建图和改板子啊,毕竟抄了这么一个大板子

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int maxn=35;//点的数量
const int N=2*maxn; 
const int maxm=5000;//2*maxn*maxn+4*maxn 源点出maxn条 汇点入maxn条 中间点两两连 
const ll INF=1e18; 
struct edge
{
   int u,v;
   ll cos,nex,w;
}e[maxm];
ll vis[N],dis[N];
ll cost[maxn][maxn];
int head[N],cnt;
void add(int u,int v,ll w,ll cos)
{
	e[cnt].v=v;
	e[cnt].w=w;
	e[cnt].cos=cos;
	e[cnt].nex=head[u];
	head[u]=cnt++;
}
void add2(int u,int v,ll w,ll cos)
{
	add(u,v,w,cos);
	add(v,u,0,-cos);
}
bool spfa(int s,int t)
{
	for(int i=0;i<2*maxn;++i)
	dis[i]=INF;
	memset(vis,0,sizeof vis);
	vis[t]=1;
	dis[t]=0;
	deque<int>q;
	q.push_back(t);
	while(!q.empty())
	{
		int u=q.front();
		vis[u]=0;
		q.pop_front();
		for(int i=head[u];~i;i=e[i].nex)
		{
			int v=e[i].v;
			if(e[i^1].w>0&&dis[v]>dis[u]-e[i].cos)
			{
				dis[v]=dis[u]-e[i].cos;
				if(!vis[v])
				{
					vis[v]=1;
					if(!q.empty()&&dis[v]<dis[q.front()])
					q.push_front(v);
					else q.push_back(v);
				}
			}
		}
	}
	return dis[s]<INF;
}
ll dfs(int u,int t,ll low,ll &tot)
{
	vis[u]=1;
	if(u==t)return low;
	ll ans=0;
	for(int i=head[u];~i;i=e[i].nex)
	{
		int v=e[i].v;
		if(!vis[v]&&e[i].w&&dis[v]==dis[u]-e[i].cos)
		{
			ll now=dfs(v,t,min(low,e[i].w),tot);
			if(now>0)
			{
				tot+=e[i].cos*now;
				low-=now;
				ans+=now;
				e[i].w-=now;
				e[i^1].w+=now;
				if(low==0)break;
			}
		}
	}
	return ans;
}
ll max_flow(int s,int t,ll &tot)//tot返回费用 函数返回值为最大流 
{
	ll ans=0;
	while(spfa(s,t))
	{
		vis[t]=1;
		while(vis[t])
		{
			memset(vis,0,sizeof vis);
			ans+=dfs(s,t,INF,tot);
		}
	}
	return ans;
} 
void init()
{
	cnt=0;
	for(int i=0;i<2*maxn;++i)
	dis[i]=INF;
	memset(head,-1,sizeof head);
	memset(vis,0,sizeof vis);
}
int main()
{
	ll tot;//总费用 
	int T,n,s,t;
	scanf("%d",&T);
	while(T--)
	{
	tot=0;
	init();
    scanf("%d",&n);
    //超级源 超级汇
    s=0;t=2*n+1; 
    //左供货 右需求
    for(int i=1;i<=n;++i)
    add2(s,i,1,0);//超级源点 
    for(int i=1;i<=n;++i)
    add2(n+i,t,1,0);//超级汇点
    for(int i=1;i<=n;++i) 
    {
    	for(int j=1;j<=n;++j)
    	{
    		scanf("%lld",&cost[i][j]);
    		add2(i,n+j,1,-cost[i][j]);//l[i]的流量 dis的费用 
    	}
    }
    max_flow(s,t,tot);
    printf("%lld\n",-tot);
    }
   return 0;
}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值