模拟赛-12 connect

题意

工厂里有 \(N\) 个机器,每个机器与其他若干个机器之间需要连线,现在要将这 \(N\) 个机器安放在 \(1\)~\(N\)\(N\) 个位置上,两个处在 \(D_i\)\(D_j\) 位置的机器连线费用为 \(|D_i-D_j|\),求最小的连线费用。

\(1\le N\le 20\)

输入

第一行两个整数 \(N,M\) 表示机器的数量和需要连线的关系数

接下来 \(M\) 行,每行 \(2\) 个数字 \(x,y\) 表示机器 \(x\) 和机器 \(y\) 之间需要连线

输出

一行一个整数表示最小花费

Solution

解法1:玄学

没想出来怎么状压只好退了个火

没想到竟然碾标算/jk

最开始就初始化一个位置数组 \(p\)\(p[i]\) 表示第 \(i\) 台机器的位置

然后每次随机交换两个位置, \(N^2\) 统计新的花费,以 \(exp(-diff/T)\) 的概率接受坏解(以后可能变优)

然后大概 \(30\) 次退火就A了/fad

#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<cmath>
using namespace std;
typedef double db;
const int maxn=27;
const db dT=0.9815;
bool m[maxn][maxn];
int p[maxn];
int N,M;
inline int abs_(const int &x) {return x>=0?x:-x;}
inline int cal()
{
	int re=0;
	for(int i=1;i<=N;i++)
		for(int j=1;j<=N;j++)
		if(m[i][j]) re+=abs_(p[i]-p[j]);
	return re>>1;
}
int ans=0x7f7f7f7f;
void SA()
{
	for(int i=1;i<=N;i++)
		p[i]=i;
	int tans=cal(),tre,p1,p2;
	db T=807;
	while(T>1e-8)
	{
		p1=rand()%N+1;p2=rand()%N+1;
		while(p1==p2) p2=rand()%N+1;
		swap(p[p1],p[p2]);
		tre=cal();
		if(tre<tans)
		{
			tans=tre;
			if(tans<ans) ans=tans;
		}
		else if(exp((tans-tre)/T)*RAND_MAX<=rand())
			swap(p[p1],p[p2]);
		else tans=tre;
		T*=dT;
	}
	return ;
}
int main()
{

	srand(19260817);
	scanf("%d%d",&N,&M);
	int u,v;
	for(int i=1;i<=M;i++)
	{
		scanf("%d%d",&u,&v);
		m[u][v]=m[v][u]=true;
	}
	for(int i=1;i<=30;i++)
		SA();
	printf("%d",ans);
	return 0;
}

用时500+ms,std用时1600+ms

解法2:正解状压

首先对于状压的一个状态 \(i\) 要理解到底是啥意思

设二进制表示下的 \(i\)\(x\)\(1\),那么这个状态表示这 \(x\)\(1\) 的位置的机器安放在了 \(1\large\text{~}x\) 位置

所以我们枚举一台未安放的机器把他放在第 \(x+1\)

假设当前状态为 \(i\) 我们枚举机器 \(j\) 放在第 \(x+1\) 位(\((i\&(1<<j))==0\)

但是这个代价就不能 \(O(N)\) 再去算一次了,不然就成了 \(O(N^2 2^N)\) 当场写假掉

我们可以计算对于每一个状态 \(i\) 有多少对冲突的关系(边的端点分别在已选中集合和未选中集合的边数),在安排机器的时候累加即可

正确性:假如第 \(i\) 次加入选中集合的机器需要和第 \(j\) 次加入选中集合的机器连边,为了方便描述我们设 \(i<j\)

那么在 \(i\) 加入选中集合之前边 \((i,j)\) 是不会累加的(因为这条边两端都在未选中集合),在 \(i\) 加入集合之后开始累加,累加到 \(j\) 加入集合为止,\(j\) 加入集合之后这条边的两端又在同一个集合中,不再累加,总共累加 \(j-i\) 次,刚好就是这个关系的花费

这个关系怎么求嘛....

首先我们先计算一个东西,状态 \(i\) 的二进制表示下 \(1\) 的个数,设为 \(sz[i]\) 递推即可: \(sz[i]=sz[i>>1]+(i\&1)\)

然后对于每一台机器 \(u\),在输入的时候就处理出一个数组 \(a[u]\) 表示需要和点 \(u\) 连接的若干台机器的二进制表示

设状态 \(i\) 的冲突关系数为 \(s[i]\) 那么有 \(s[i]=\sum\limits_{j=0}^{N}sz[i\&a[j]]\) \(((1<<j)\& i==0)\)

(找出一个没选的数字,然后把既和他要连边也已经选取的数字累加进去)

状态转移方程: \(F[i|(1<<j)]=F[i]+s[i]\)

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=(1<<20)+7;
int F[maxn],a[maxn],sz[maxn],s[maxn];
int N,M;
int all;
int main()
{
	scanf("%d%d",&N,&M);
	int u,v;
	for(register int i=1;i<=M;i++)
	{
		scanf("%d%d",&u,&v);
		a[u-1]|=(1<<v-1);
		a[v-1]|=(1<<u-1);
	}
	all=(1<<N)-1;
	for(register int i=1;i<=all;i++) sz[i]=sz[i>>1]+(i&1);
	for(register int i=1;i<=all;i++)
		for(register int j=0;j<N;j++)
			if((i&(1<<j))==0) s[i]+=sz[a[j]&i];
	memset(F,0x3f,sizeof(F));
	F[0]=0;
	for(register int i=0;i<=all;i++)
		for(register int j=0;j<N;j++)
			if(((1<<j)&i)==0)
				F[i|(1<<j)]=min(F[i|(1<<j)],F[i]+s[i]);
	printf("%d",F[all]);
	return 0;
}

鸽完了/cy

posted @ 2020-06-26 10:57  ShadderLeave  阅读(107)  评论(0编辑  收藏  举报