题解 P3959 【宝藏】
sermoon
2018-01-31 00:36:28
本题大致有以下几种思路:
1.超纲的模拟退火
2.错误但可以A题的状压DP
3.正确但跑得太慢的状压DP
但是,这些真的正确或者适合吗?
想当初在NOIP考场上打爆搜结果编译不过的尴尬;
不过感谢上苍我还有5分。
但现在,
我们从起点开始走起;
20分做法:
容易想到利用树这一条件
用dfs/bfs求出距离即可
40分做法:
类比与20分做法
用Floyd求出最短路即可(实际上没必要使用SPFA)
我们容易发现这题的n非常非常小
大多数的人的第一印象恐怕就是复杂度常常是(2 ^ n)的状压吧
但是啊,NOIP还有一种神奇的算法,虽然使用它算不上高大大气上档次,复杂度高到无法接受,但是,它毕竟是你曾近学过的算法。
它是谁?复杂度O(n!/玄学)的dfs!!!
70分做法:
我们发现 8!非常的小,带上一大串常数都没问题
因此我们考虑深搜
如何爆搜?
第一反应是枚举全排列然后去更新答案。
但是有问题啊,知道点开拓的顺序并没有什么用,我们还要知道它是由哪个点开拓而来的。
因此我们再枚举去开拓的点:
代码如下
```cpp
#include <cstdio>
using namespace std;
int a[55], l[55];
int cost[55][55];
int ans = 2147483645, tmp, n, m;
#define getchar() (S==T&&(T=(S=BB)+fread(BB,1,1<<15,stdin),S==T)?EOF:*S++)
char BB[1 << 16], *S = BB, *T = BB;
inline int read () {
int X = 0; char ch = 0;
while(ch < 0 || ch > 9) ch = getchar();
while(ch >= 0 && ch <= 9) X = X * 10 + ch - '0', ch = getchar();
return X;
}
inline int min(int a, int b) {
int c = (a - b) >> 31;
return a ^ c | b ^ ~c;
}
inline void dfs(int e, int num) {
if(num == n) {
ans = min(tmp, ans);
return;
}
if(tmp >= ans) return;
for(int i = 1; i <= n; i ++) {//确定下一步扩展谁
if(l[i]) continue;
for(int j = 1; j <= n; j ++) {//由谁扩展
if(cost[j][i] == 2147483645 || !l[j] || i == j) continue;
tmp += l[j] * cost[j][i]; l[i] = l[j] + 1;
dfs(i, num + 1);
tmp -= l[j] * cost[j][i]; l[i] = 0;
}
}
}
int main() {
int u, v;
n = read(); m = read();
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
cost[i][j] = 2147483645;
for(int i = 1; i <= m; i ++)
u = read(), v = read(),
cost[u][v] = cost[v][u] = min(cost[u][v], read());
for(int i = 1; i <= n; i ++) {
l[i] = 1; dfs(i, 1); l[i] = 0;
}
printf("%lld\n", ...);
return 0;
}
```
(请不要尝试着复制本代码)
为什么这个爆搜正确呢?
因为题目中一句及其重要的话:
**另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。**
因此,我们其实可以很轻易地拿到70分。
大家在考场上都拿了几分呢?
100分不超纲极快解法:
既然都写了爆搜,我们想想有没有更快的速度?
有,我们还有剪枝呢!!
dfs + 剪枝可以跑多快? 玄学啊。
什么? 你想知道玄学有多快?
说了不要吃惊。 0ms。
那让我们结合详细的注释来看看如何剪枝。
```cpp
#include <cstdio>
//基础的头文件
#include <algorithm>
//给sort开的头文件
#define INF 1917483645
using namespace std;
int vis[20], lev[20], d[20];
//vis : 已经访问过的点
//lev : 点距离初始点的距离
//d : 通过此点可以达到的点数
int c[20][20], tar[20][20];
//c : 费用
//tar :存下每个点能到的点
/*为什么要特意存下每个点到的点?
答案其实并不困难,加快访问速度*/
int ans = INF, tmp, tot, cnt, n, m, p;
//ans : 最终的答案
//tmp : 最优解剪枝用
//tot : dfs储存中间答案的用具
//cnt :现在已经访问过的点数
//n : 点数
//m : 边数
//p : sort用品
inline bool cmp(int a, int b) {
return c[p][a] < c[p][b];
//按照费用给每个点能到的点排序
}
inline void dfs(int num, int node) {
for(int i = num; i <= cnt; i ++) {
//由第几个被访问的点来扩展
if(tot + tmp * lev[vis[i]] >= ans) return;
//最优性剪枝,如果当前解加上理论最小的费用都比中途的答案高
//那么这次搜索一定不是最优解
for(int j = node; j <= d[vis[i]]; j ++)
//下一步扩展谁
if(!lev[tar[vis[i]][j]]) { //用lev同时充当标记,有lev代表已访问
cnt ++; //多添加一个点
vis[cnt] = tar[vis[i]][j]; //记录这个点
tmp -= c[vis[cnt]][tar[vis[cnt]][1]]; //理论最小的更新
tot += c[vis[i]][vis[cnt]] * lev[vis[i]]; //加上当前的影响
lev[vis[cnt]] = lev[vis[i]] + 1; //因为从vis[i]拓展,故lev一定为其 + 1
dfs(i, j + 1); //继续从i枚举, 注意到tar[i][1 ~ j]已全部访问,下一次从j + 1尝试访问
tot -= c[vis[i]][vis[cnt]] * lev[vis[i]]; //回溯
lev[vis[cnt]] = 0; //回溯
tmp += c[vis[cnt]][tar[vis[cnt]][1]]; //回溯
cnt --; //回溯
}
node = 1; //剪枝别剪错了,i枚举玩下次还要从1枚举
}
if(cnt == n) { //已经访问了n个点,更新答案
if(tot < ans) ans = tot;
return;
}
}
int main() {
int u, v, w;
scanf("%d%d", &n, &m); //读入n, m
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
c[i][j] = INF; //初始赋无限大,方便后面的操作
for(int i = 1; i <= m; i ++) {
scanf("%d%d%d", &u, &v, &w); //这个w先暂时存起来
//因为发现m = 1000 远> n * n,所以要剪掉一些边
if(c[u][v] < w) continue; //w不能更新c[u][v]
if(c[u][v] == INF) //第一次更新c[u][v],统计u,v的出度
tar[u][++ d[u]] = v, tar[v][++ d[v]] = u;
c[u][v] = c[v][u] = w;
}
for(int i = 1; i <= n; i ++) {
p = i; //sort排序cmp
sort(tar[i] + 1, tar[i] + 1 + d[i], cmp);
tmp += c[i][tar[i][1]]; //因为每个点总要扩展一条边,
//因此我们把最小的边找出来,作为最优性剪枝
//不仅如此,因为边越小作为最终解的可能性越大
//实际上在一定程度上优化了程序
}
for(int i = 1; i <= n; i ++) {
tot = 0; cnt = 1; //赋初值
vis[1] = i; //第一个就选i
tmp -= c[i][tar[i][1]]; //i的最优性剪枝的影响要去掉
//因为剪枝的时候是以还未访问和还未将被访问的点为基础的
//不去掉剪枝就是错误的
lev[i] = 1; //赋值
dfs(1, 1); //只有一个点,也肯定是从1枚举
lev[i] = 0; //回溯
tmp += c[i][tar[i][1]];
}
printf("%d", ans);
return 0;
}
```
希望大家学习高级算法后不要忘了那些低级算法的重要!!
By Xiyue reverymoon