COCI 2016/2017 CONTEST #3 - Kronican
题意
有(n)个无限体积的杯子,里面都有一些水,Mislav想喝掉所有的水,但他只想喝最多(k)杯水
所以他需要将这(n)杯水进行合并,将第(i)杯水倒进第(j)杯所需要的花费为(C_{i,j})
问喝到所有水的最小花费
限制
(1leq kleq nleq 20)
(0leq C_{i,j}leq 10^5)
思路一(最小树形图)
由于数据范围只有(20),首先可以枚举出最后剩下的(k)杯水是哪些,最多的情况数为(C_{20}^{10})
- 可以以(0/1)两种状态表示每个杯子最终状态,存在一个长度为(n)的数组内,以next_permutation来遍历所有(C_n^k)种情况即可,也可通过二进制枚举方法等。
对于枚举出来的每一个状态,确定了最后“剩下的水杯”是哪些。
将水杯看作一张有向图中的节点,就表示需要找出一张最小树形图(森林),使得每个“空水杯”顺着箭头走最终都会指向那些“剩下的水杯”。
又贪心可得,“剩下的水杯”严格为(k)杯时答案最优,所以这可能是一张包含(k)棵有向树的森林,故需要建立一个虚根,让所有“剩下的水杯”指向这个虚根,再对虚根做一遍最小树形图即可。
- 下图为(n=11,k=3)时的树形图(注意在跑最小树形图算法时应反向建边)
程序一
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
struct Edge{
int u,v,dis;
Edge(){}
Edge(int u,int v,int dis):u(u),v(v),dis(dis){}
};
struct Directed_MT{
int n,m;
Edge edges[400];
int vis[25],pre[25],id[25],in[25];
void init(int n){
this->n=n;
m=0;
}
void addedge(int u,int v,int dis){
edges[m++]=Edge(u,v,dis);
}
int DirMt(int root){
int ans=0;
while(1){
for(int i=0;i<n;i++)in[i]=INF;
for(int i=0;i<m;i++){
int u=edges[i].u,v=edges[i].v;
if(edges[i].dis<in[v]&&u!=v){
in[v]=edges[i].dis;
pre[v]=u;
}
}
for(int i=0;i<n;i++){
if(i==root)continue;
if(in[i]==INF)return -1;
}
int cnt=0;
memset(id,-1,sizeof(id));
memset(vis,-1,sizeof(vis));
in[root]=0;
for(int i=0;i<n;i++){
ans+=in[i];
int v=i;
while(vis[v]!=i&&id[v]==-1&&v!=root){
vis[v]=i;
v=pre[v];
}
if(v!=root&&id[v]==-1){
for(int u=pre[v];u!=v;u=pre[u])
id[u]=cnt;
id[v]=cnt++;
}
}
if(cnt==0)break;
for(int i=0;i<n;i++)
if(id[i]==-1)id[i]=cnt++;
for(int i=0;i<m;i++){
int v=edges[i].v;
edges[i].v=id[edges[i].v];
edges[i].u=id[edges[i].u];
if(edges[i].u!=edges[i].v)
edges[i].dis-=in[v];
}
n=cnt;
root=id[root];
}
return ans;
}
}MT;
int n,k,a[30];
int cost[30][30];
int solve()
{
MT.init(n+1);
for(int i=1;i<=n;i++)
{
if(a[i]==1)
MT.addedge(n,i-1,0); //注意编号从0开始,故全部-1
}
for(int i=1;i<=n;i++)
if(a[i]==0)
{
for(int j=1;j<=n;j++)
if(i!=j)
MT.addedge(j-1,i-1,cost[i][j]); //建立反向边
}
return MT.DirMt(n);
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&cost[i][j]);
for(int i=1;i<=n-k;i++)
a[i]=0;
for(int i=n-k+1;i<=n;i++)
a[i]=1;
int ans=INF;
do{
ans=min(ans,solve());
}while(next_permutation(a+1,a+1+n));
printf("%d
",ans);
return 0;
}
思路二(状压DP)
以二进制存储当前(n)个杯子是否有水
每一次倒水一定是由“有水的杯子”倒向“有水的杯子”
故可以枚举状态(S),二进制上以(1)代表有水,以(0)代表无水
那么对于每一种状态(S),枚举其中两个有水的杯子(i,j),表示此时枚举的是将(i)杯子中的水倒入(j)杯子中
倒完后,(i)杯子将会成为空杯子,故此时是由状态(S)转移到状态(S xor 2^i)(将(S)中第(i)个位置的(1)变为(0))的,得到状态转移方程为
[dp[S xor (1<<i)]=min(dp[S xor (1<<i)], dp[S]+cost[i][j])
]
由于枚举的(i,j)两位置在(S)内保证为(1)(有水),故(S xor 2^ilt S)一定成立,所以对于状态(S)只需要从大到小枚举即可(自(2^n-1)到(0))
程序二
#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
int n,k,cost[25][25];
int dp[1<<21];
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&cost[i][j]);
dp[(1<<n)-1]=0; //初始状态花费为0
for(int S=(1<<n)-2;S>=0;S--)
dp[S]=INF;
for(int S=(1<<n)-1;S>=0;S--) //枚举状态S
{
for(int i=0;i<n;i++) //枚举倒出的杯子
{
if(S&(1<<i)) //倒出的杯子中有水
{
for(int j=0;j<n;j++) //枚举倒入的杯子
{
if(i==j)
continue;
if(S&(1<<j)) //倒入的杯子中有水
dp[S^(1<<i)]=min(dp[S^(1<<i)],dp[S]+cost[i][j]);
}
}
}
}
int ans=INF;
for(int S=(1<<n)-1;S>=0;S--)
if(__builtin_popcount(S)==k) //如果剩下的有水杯子个数为k,取一次答案
ans=min(ans,dp[S]);
printf("%d
",ans);
return 0;
}