模拟赛-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