P3959 [NOIP2017 提高组] 宝藏 题解
P3959 [NOIP2017 提高组] 宝藏 题解
搜索魅力时刻
怎么说,四种做法
- 比较??的模拟退火
- 跑得快但是 正确性有问题的 状压DP
- 跑得慢但是 一定正确的 状压 DP
- 时间复杂度很玄学的DFS+剪枝
我就选择了搜索的做法
先打个暴搜,70pts
点击查看暴搜代码
#include <bits/stdc++.h>
using namespace std;
const int inf = 998244353;
int n, m;
int dis[100][100];
struct node
{
// int vis;
int step;
};
node nds[114514];
int tmp;
int ans = inf;
void dfs(int num)
{
if (tmp > ans)
{
return;
}
if (num == n)
{
ans = min(ans, tmp);
}
else
{
for (int yy = 1; yy <= n; yy++) //由谁拓展
{
if (nds[yy].step)
{
for (int tt = 1; tt <= n; tt++) //拓展到谁
{
if (nds[tt].step == 0 && yy != tt && dis[yy][tt] != inf)//如果能去
{
tmp += nds[yy].step * dis[yy][tt];
nds[tt].step = nds[yy].step + 1;
dfs(num + 1);
tmp -=nds[yy].step * dis[yy][tt];
nds[tt].step=0;
}
}
}
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin >> n >> m;
memset(dis, inf, sizeof(dis));
int a, b, c;
for (int yy = 1; yy <= m; yy++)
{
cin >> a >> b >> c;
if (dis[a][b] < c)
{
continue;
}
if (dis[a][b] > c)
{
dis[b][a] = c;
dis[a][b] = c;
}
}
for (int ww = 1; ww <= n; ww++) //枚举起点
{
nds[ww].step = 1;
dfs(1);
nds[ww].step = 0;
}
cout<<ans;
return 0;
}
接下来就是想着怎么优化了
自然,我们可以想到这题可以加上一个最优化剪枝,然后在记录一下哪些点被拓展过。
就可以得到如下代码
点击查看优化代码-1
#include <bits/stdc++.h>
using namespace std;
const int inf = 198244353;
int n, m;
int dis[100][100], sho[100];
int dd[55];
struct node
{
// int vis;
int step;
};
node nds[114514];
int tmp;
int ans = inf;
int short_;
void dfs(int num)
{
// cout<<(3*ans)/2<<endl;
if (num == n)
{
ans = min(ans, tmp);
}
else
{
for (int ysy = 1; ysy <= num; ysy++) //由谁拓展
{
int yy = dd[ysy];
//最优化剪枝
if (tmp + short_ * nds[yy].step > ans)//如果当前代价加上后面的最小代价还是比之前的答案大,那么一定不优
{
return;
}
for (int tt = 1; tt <= n; tt++) //拓展到谁
{
if (nds[tt].step == 0 && yy != tt && dis[yy][tt] != inf)
{
tmp += nds[yy].step * dis[yy][tt];
nds[tt].step = nds[yy].step + 1;
short_ -= sho[tt];//减去这个点的最小代价
dd[num + 1] = tt;
dfs(num + 1);
dd[num + 1] = 0;
short_ += sho[tt];
tmp -= nds[yy].step * dis[yy][tt];
nds[tt].step = 0;
}
}
}
}
}
int main()
{
// freopen("P3959_1.in", "r", stdin);
// freopen("", "w", stdout);
ios::sync_with_stdio(false);
cin >> n >> m;
memset(dis, inf, sizeof(dis));
int a, b, c;
for (int yy = 1; yy <= m; yy++)
{
cin >> a >> b >> c;
// if (dis[a][b] < c)
// {
// continue;
// }
// if (dis[a][b] > c)
dis[b][a] = min(dis[b][a], c);
dis[a][b] = min(dis[a][b], c);
}
for (int yy = 1; yy <= n; yy++)
{
int ww = inf;
for (int ee = 1; ee <= n; ee++)//获取每个点的最小代价,也就是那个最短的出边
{
if (yy != ee)
{
ww = min(ww, dis[yy][ee]);
}
}
short_ += ww;
sho[yy] = ww;
}
for (int ww = 1; ww <= n; ww++) //枚举起点
{
tmp = 0;
nds[ww].step = 1;
short_ -= sho[ww];
dd[1] = ww;
dfs(1);
dd[1] = 0;
short_ += sho[ww];
nds[ww].step = 0;
// cout << ans << "\n";
}
cout << ans;
return 0;
}
官方数据都过了,但是 Hack 数据会有 \(10\) 各点左右 超时,那我们该怎么进一步优化呢?
首先我们可以发现,当我们拓展完一个节点后,又会回到之前拓展过的节点上,重新遍历他们的出度,但是事实上,这些点中的出度已经有很多被遍历过了,所以我们可以用当前弧优化的思想 对于每一个点记录一下这次搜索之中已经遍历到哪个出度了。
加上这个优化的代码
#include <bits/stdc++.h>
using namespace std;
const int inf = 998244353;
int n, m;
int dis[100][100], sho[100];
int du[100];
int to_[100][100];
int dd[55];
struct node
{
int vis=1;
int step;
};
node nds[114];
int tmp;
int ans = inf;
int short_;
int cmm;
bool cmp(int a, int b)
{
return dis[cmm][a] < dis[cmm][b];
}
void dfs(int num)
{
// cout<<(3*ans)/2<<endl;
if (num == n)
{
ans = min(ans, tmp);
}
else
{
int tt, yy;
for (int ysy = 1; ysy <= num; ysy++) //由谁拓展
{
yy = dd[ysy];
if (tmp + short_ * nds[yy].step > ans)
{
return;
}
// cout<<du[yy]<<" ||| "<<endl;
for (int att = nds[yy].vis; att <= du[yy]; att++) //拓展到谁
{
nds[yy].vis++;
tt = to_[yy][att];
// cout<<tt;
if (nds[tt].step == 0&&dis[yy][tt]!=inf)
{
tmp += nds[yy].step * dis[yy][tt];
nds[tt].step = nds[yy].step + 1;
short_ -= sho[tt];
dd[num + 1] = tt;
dfs(num + 1);
dd[num + 1] = 0;
short_ += sho[tt];
tmp -= nds[yy].step * dis[yy][tt];
nds[tt].step = 0;
}
}
nds[yy].vis=1;//C
}
}
}
int main()
{
// freopen("P3959_22.in", "r", stdin);
// freopen("", "w", stdout);
ios::sync_with_stdio(false);
cin >> n >> m;
for (int ww = 0; ww <= n + 1; ww++)
{
for (int ee = 0; ee <= n + 1; ee++)
{
dis[ww][ee] = inf;
}
du[ww]=0;
}
int a, b, c;
for (int yy = 1; yy <= m; yy++)
{
cin >> a >> b >> c;
// if (dis[a][b] < c)
// {
// continue;
// }
// if (dis[a][b] > c)
if (dis[a][b] == inf)
{
du[a]++;
du[b]++;
to_[a][du[a]] = b;
to_[b][du[b]] = a;
}
dis[b][a] = min(dis[b][a], c);
dis[a][b] = min(dis[a][b], c);
}
for (int yy = 1; yy <= n; yy++)
{
// cout<<du[yy]<<endl;;
cmm = yy;
sort(to_[yy] + 1, to_[yy] + 1 + du[yy], cmp);
int ww = inf;
for (int ee = 1; ee <= n; ee++)
{
if (yy != ee)
{
ww = min(ww, dis[yy][ee]);
}
}
short_ += ww;
sho[yy] = ww;
}
for (int ww = 1; ww <= n; ww++) //枚举起点
{
tmp = 0;
nds[ww].step = 1;
short_ -= sho[ww];
dd[1] = ww;
dfs(1);
dd[1] = 0;
short_ += sho[ww];
nds[ww].step = 0;
// cout << ans << "\n";
}
cout << ans;
return 0;
}
加上这个优化之后,我们就可以过掉这个题了,但是,还是太慢了,需要足足 \(1.3s\) 才能跑完官方的数据。
事实上,我们还有 \(1\) 个很可以优化的地方:对于最先开始遍历的那些点,我们在循环之中已经把他们都处理完了,下次搜索就不需要再考虑他们,于是,我们再次用上当前弧优化的思想,加上一个传参 来记录有多少个被拓展的点已经被处理完了,加上这个优化之后 ,真就是暴力碾标算,只用 \(130ms\) 就可以跑完官方的所有数据
最终版的代码
#include <bits/stdc++.h>
using namespace std;
const int inf = 998244353;
int n, m;
int dis[100][100], sho[100];
int du[100];
int to_[100][100];
int dd[55];
/*
dis:邻接矩阵
sho:每个点最短的出边
du:每个点的出度(边已去重)
to_:每个点能到的点
dd:被拓展的点的顺序
*/
struct node
{
int vis = 1;
int step;
};
node nds[114];//每个洞穴
int tmp;//dfs用的,记录中间答案
int ans = inf;//最终答案
int short_;//每个点最短的出度之和,用于最优化剪枝
int cmm;//sort用
bool cmp(int a, int b)
{
return dis[cmm][a] < dis[cmm][b];
}
void dfs(int now_,int num)
{
// cout<<(3*ans)/2<<endl;
if (num == n)
{
ans = min(ans, tmp);
}
else
{
int tt, yy;
for (int ysy = now_; ysy <= num; ysy++) //由谁拓展
{
yy = dd[ysy];
if (tmp + short_ * nds[yy].step > ans)
{
return;
}
for (int att = nds[yy].vis; att <= du[yy]; att++) //拓展到谁
{
nds[yy].vis++;
tt = to_[yy][att];
if (tt == 0)
{
cout << "Warning!";
cout << yy << "'s " << att << '\n';
}
if (nds[tt].step == 0)
{
tmp += nds[yy].step * dis[yy][tt];
nds[tt].step = nds[yy].step + 1;
short_ -= sho[tt];
dd[num + 1] = tt;
dfs(ysy,num + 1);
dd[num + 1] = 0;
short_ += sho[tt];
tmp -= nds[yy].step * dis[yy][tt];
nds[tt].step = 0;
}
}
nds[yy].vis = 1;//这个点处理完了,将其当前弧优化归零
}
}
}
int main()
{
// freopen("P3959_22.in", "r", stdin);
// freopen("", "w", stdout);
ios::sync_with_stdio(false);
cin >> n >> m;
for (int ww = 0; ww <= n + 1; ww++)
{
for (int ee = 0; ee <= n + 1; ee++)
{
dis[ww][ee] = inf;
}
dis[ww][ww]=0;
du[ww] = 0;
}
int a, b, c;
for (int yy = 1; yy <= m; yy++)
{
cin >> a >> b >> c;
if (dis[a][b] == inf && c < inf)
{
du[a]++;
du[b]++;
to_[a][du[a]] = b;
to_[b][du[b]] = a;
}
dis[b][a] = min(dis[b][a], c);
dis[a][b] = min(dis[a][b], c);
}
for (int yy = 1; yy <= n; yy++)
{
cmm = yy;
sort(to_[yy] + 1, to_[yy] + 1 + du[yy], cmp);//排序,降低复杂度,因为越短的边成为答案的概率越大
int ww = inf;
for (int ee = 1; ee <= n; ee++)//找到最小的出度(其实这里可以直接用上排序后的结果)
{
if (yy != ee)
{
ww = min(ww, dis[yy][ee]);
}
}
short_ += ww;
sho[yy] = ww;
}
for (int ww = 1; ww <= n; ww++) //枚举起点
{
tmp = 0;
nds[ww].step = 1;
short_ -= sho[ww];
dd[1] = ww;
dfs(1,1);
dd[1] = 0;
short_ += sho[ww];
nds[ww].step = 0;
// cout << ans << "\n";
}
cout << ans;
return 0;
}
提交记录: