UVA10911 Forming Quiz Teams

版权声明:听说这里让写版权声明~~~ https://blog.csdn.net/m0_37691414/article/details/84501975

复杂状态的动态规划


最优配对问题。空间里有n个点P 0 ,  P 1 , … , P n-1 ,你的任务是把它们配成n/2对(n是偶数),使得每个点恰好在一个点对中。所有点对中两点的距离之和应尽量小。n≤20, |x i |,|y i |,|z i |≤10000。


分析

分析来自紫书详细见紫书。。。
既然每个点都要配对,很容易把问题看成如下的多阶段决策过程:先确定P 0 和谁配对,然后是P 1 ,接下来是P 2 ,……,最后是P n-1 。按照前面的思路,设d(i)表示把前i个点两两配对的最小距离和,然后考虑第i个点的决策——它和谁配对呢?假设它和点j配对(j<i),那么接下来的问题应是“把前i-1个点中除了j之外的其他点两两配对”,它显然无法用任何一
个d值来刻画——此处的状态定义无法体现出“除了一些点之外”这样的限制。当发现状态无法转移后,常见的方法是增加维度,即增加新的因素,更细致地描述状态。既然刚才提到了“除了某些元素之外”,不妨把它作为状态的一部分,设d(i,S)表示把前i个点中,位于集合S中的元素两两配对的最小距离和,则状态转移方程为:其中,|P i P j |表示点P i 和P j 之间的距离。方程看上去很不错,但实现起来有问题:如何表示集合S呢?由于它要作为数组d中的第二维下标,所以需要用整数来表示集合,确切地说,是{0, 1, 2,…,n-1}的任意子集(subset)。

for(int i = 0; i < n; i++)
    for(int S = 0; S < (1<<n); S++) {
        d[i][S] = INF;
        for(int j = 0; j < i; j++) if(S & (1<<j))
        d[i][S] = min(d[i][S], dist(i, j) + d[i-1][S^(1<<i)^(1<<j)]);
    }



上述程序中故意用了很多括号,传达给读者的信息是:位运算的优先级低,初学者很容易弄错。例如,“1<<n-1”的正确解释是“1<<(n-1)”,因为减法的优先级比左移要高。为了保险起见,应多用括号。另一个技巧是利用C语言中“0为假,非0为真”的规定简化表达式:“if(S & (1<<j))”的实际含义是“if((S & (1<<j)) != 0)”。

由于大量使用了形如1<<n的表达式,此类表达式中,左移运算符“<<”的含义是“把各个位往左移动,右边补0”。根据二进制运算法则,每次左移一位就相当于乘以2,因此a<<b相当于a*2^{b} ,而在集合表示法中,1<i代表单元素集合{i}。由于0表示空集,“S & (1<<j)”不等于0就意味着“S和{j}的交集不为空”。上面的方程可以进一步简化。事实上,阶段i根本不用保存,它已经隐含在S中了——S中的最大元素就是i。这样,可直接用d(S)表示“把S中的元素两两配对的最小距离和”,
则状态转移方程为:

d(S)=min{|PiPj |+d(S-{i}-{j})|j∈S, i=max{S}}


状态有2^{n} 个,每个状态有O(n)种转移方式,总时间复杂度为O(n2^{n} )。

值得一提的是,不少用户一直在用这样的状态转移方程:

d(S)=min{|PiPj |+d(S-{i}-{j})|i, j∈S, }


它和刚才的方程很类似,唯一的不同是:i和j都是需要枚举的。这样做虽然也没错,但每个状态的转移次数高达O(n 2 ),总时间复杂度为O(n^{2}2^{n} ),比刚才的方法慢。这个例子再次说明:即使用相同的状态描述,减少决策也是很重要的。

接下来出现了一个新问题:如何求出S中的最大元素呢?用一个循环判断即可。当S取遍{0, 1, 2,…, n-1}的所有子集时,平均判断次数仅为2。

for(int S = 0; S < (1<<n); S++) {
    int i, j;
    d[S] = INF;
    for(i = 0; i < n; i++)
        if(S & (1<<i)) break;
    for(j = i+1; j < n; j++)
        if(S & (1<<j)) d[S] = min(d[S], dist(i, j) + d[S^(1>>i)^(1>>j)]);
}


注意,在上述的程序中求出的i是S中的最小元素,而不是最大元素,但这并不影响答案。另外,j的枚举只需从i+1开始——既然i是S中的最小元素,则说明其他元素自然均比i大。最后需要说明的是S的枚举顺序。不难发现:如果S'是S的真子集,则一定有S'<S,因此若以S递增的顺序计算,需要用到某个d值时,它一定已经计算出来了。

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1010;
int x[maxn], y[maxn];
double d[1<<16];
double dis(int a,int b){
    return sqrt((x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]));
}
int n, kase;
int main(){
	freopen("data.in", "r", stdin);
	while(scanf("%d", &n) == 1 && n){
		char s[maxn];
		n <<= 1;
		for(int i = 0; i < n; ++i){scanf("%s%d%d", s, &x[i], &y[i]);}
		d[0] = 0;
		for(int S = 1; S < (1 << n); ++S){
			d[S] = INF;
			int i, j;
			for(i = 0; i < n; ++i) if(S & (1 << i)) break;
			for(j = i + 1; j < n; ++j) if(S & (1 << j)) d[S] = min(d[S], dis(i, j) + d[S ^ (1 << i) ^ (1 << j)]);
		}
		printf("Case %d: %.2lf\n", ++kase, d[(1 << n) - 1]);
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/m0_37691414/article/details/84501975