模拟赛毒瘤状压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;
}
原文地址:https://www.cnblogs.com/MisakaMKT/p/11252278.html