模拟赛毒瘤状压DP题:Kronican
Kronican
内存限制:32 MiB
时间限制:2000 ms
标准输入输出
题目类型:传统
评测方式:文本比较
上传者: cqbzgm
题目描述
Mislav有N个无限体积的杯子,每一个杯子中都有一些水。Mislav想喝掉所有的水,但他不想喝超过K杯水。Mistrav能做的就是将一个杯子中的水倒入另一个杯子中。 不幸的是,挑选哪两个杯子进行倒水操作对Mislav来说很重要,因为并非所有的杯子都离他一样远。更准确地说,从i号杯子向j号杯子倒水所付出的代价为Cij。 帮助Mislav找到他需要付出的总代价的最小值。输入格式
第一行输入包含整数N和K(1≤K≤N≤20)。表示水杯的总数和Mislav最多能喝多少杯。 接下来N行每行包含N个整数Cij(0≤Cij≤1e5)。第i+1行的第j个整数表示从第i个杯子第j个杯子倒水所需要付出的代价。保证Cii等于0。输出格式
输出一个整数。表示Mislav需要付出的总代价的最小值。样例
样例输入1
3 3
0 1 1
1 0 1
1 1 0
样例输出1
0
样例输入2
3 2
0 1 1
1 0 1
1 1 0
样例输出2
1
样例输入3
5 2
0 5 4 3 2
7 0 4 4 4
3 3 0 1 2
4 3 1 0 5
4 5 5 5 0
样例输出3
5
数据范围与提示
对于40%的数据,N≤10。
这道题考场上我想到的居然是最小生成树(于是就成功的爆零了),如果加了特判的话就有80分,主要还是数据太水了吧。
下面就来讲讲正解呗,因为之前接触过状压DP,所以做起来还是比较顺。
一个二进制数\(0000\)表示4个水杯都装有水,\(0001\)表示第一个水杯是空的,代价最小的状态,同理,\(1000\)表示第4个水杯是空的,代价最小的状态。
现在来考虑状态转移,拿\(1000\)举例,不难发现它可以转移到。
- \(1100\)
- \(1010\)
- \(1001\)
(可以这么考虑,我们在\(1000\)这个状态下进行了一个将\(i\)杯子的水倒入了\(j\)的操作。不管\(i\)和\(j\)是多少,总会有一个杯子会空掉,所以会多出一个"1"出来)
因为每次操作只能在两个装有水的杯子\(i\)和\(j\)的杯子进行,所以我们只需要在当前状态\(1000\)里暴力枚举位数为\(0\)的\(i\)和\(j\),模拟将\(i\)倒入\(j\)的操作。因为\(i\)倒入了\(j\),所以第\(i\)位为1,权值为\(w_{ij}\)。
那么我们便可以得出状态转移方程:
\(f_{S+1<<i} = min(f_{S}+w_{ij})\)
S为当前状态,一个二进制数。当空的杯子,也就是S的1的个数为\(n-k\)时,我们便可以记录答案。
代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define N 30
int n,k,w[N][N],ans,inf,f[1<<21];
int main() {
memset(f,0x3f,sizeof(f));
ans=f[0];
cin>>n>>k;
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
cin>>w[i][j];
f[0]=0;
for(int S=0;S<1<<n;S++) {
int sum=0;
for(int i=1;i<=n;i++) if(S & (1<<(i-1))) sum++;
if(sum==n-k) {
ans=min(ans,f[S]);
continue;
}
if(sum>n-k) continue;
for(int i=1;i<=n;i++)
if(!(S&(1<<(i-1))))
for(int j=1;j<=n;j++)
if(!(S & (1<<(j-1))) && i!=j)
f[S+(1<<(i-1))]=min(f[S+(1<<(i-1))],f[S]+w[i][j]);
}
cout<<ans;
}