题意
思考
终于在NOIp2018前一天写完了这一道状压。
怎么说呢,这一题实现起来不是太难,思路也不算是太难,如果想到状压,想到转移方程的话基本上就可做了。
最小生成树的错误在于它没有考虑到深度对答案的影响,本题另外一个模拟退火的解法也不能算是真正意义上的正解(蒟蒻认为状压还是此题的最好解法,别喷我 (QWQ) )
考虑如何设计状态:
- 当前点集我们肯定要记录
- 如果我们记录当前在哪个点,这似乎不太好转移,再仔细看答案与 (k) 有关,即当前点离根还有多少个点,这不就是深度吗,我们可以按深度,一层一层地转移
我们用 (f[i][S]) 表示当前深度为 (i), 当前点集为 (S) 的最小花费,转移方程:
[f[i][S] = min_{S_1subseteq S}{f[i-1][S_1] + (i-1)*cost_{S_1 o S}}
]
其中, (cost) 表示从状态 (S_1) 转移到 (S) 的最短距离,这个我们先预处理出来,于是我们就可以愉快 (A) 掉它了,(ps:) 复杂度不是很会算
代码
#include<bits/stdc++.h>
using namespace std;
int read(){
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x * f;
}
const int N = 13;
const int oo = 0x3f3f3f3f;
int n, m, S;
int f[N][1<<12], dis[N][N], MINd[1<<12][1<<12];
int main(){
n = read(), m = read(); S = (1 << n) - 1;
memset(dis, 0x3f, sizeof(dis));
memset(f, 0x3f, sizeof(f));
for(int i=1; i<=m; i++){
int u = read(), v = read(), d = read();
dis[u][v] = dis[v][u] = min(dis[u][v], d);
}
for(int i=0; i<=S; i++){//枚举状态
for(int j=i; j; j=(j-1)&i){//枚举子集
int tmp = i ^ j; //需要计算的点
for(int k=0; k<=n-1; k++){
if(tmp & (1 << k)){//此时计算k+1这个点
int nowd = oo;
for(int p=0; p<=n-1; p++){//枚举起点
if(j & (1 << p)){
nowd = min(nowd, dis[p+1][k+1]);
}
}
if(nowd == oo) {
MINd[j][i] = nowd;
continue;
}
MINd[j][i] += nowd;
}
}
}
}//以上是对子集与子集间最小路程的预处理
for(int i=0; i<=n-1; i++){
f[1][1<<i] = 0;
}
for(int i=2; i<=n; i++){
for(int j=0; j<=S; j++){//枚举状态
for(int k=j; k; k=(k-1)&j){
if(MINd[k][j] < oo){
f[i][j] = min(f[i][j], f[i-1][k] + (i-1)*MINd[k][j]);
}
}
}
}
int ans = oo;
for(int i=1; i<=n; i++) ans = min(ans, f[i][S]);
cout << ans;
return 0;
}
总结
要设计出易于转移的状态,例如这一题,转移时的中间量是与深度有关的,所以状态中存了一维代表深度。
还有一个小 (trick) :在枚举一个点集的子集时,如果暴力枚举肯定 (GG),我们可以这样枚举(这里枚举的是非空子集,可以自行模拟一遍)
for(int i=S; i; i=(i-1)&S)