牛客CSP-S模拟赛 十二桥问题
%%% Lance1ot 三秒就把这题切了
但既然标签有个“乱搞”,就肯定不是用的官方正解 233
考虑折半搜索(中途相遇)
将必须经过的\(k\)条边所连的点称为关键点
先以所有关键点为起点,分别跑一边最短路
设\(mid = \lfloor \frac{k}{2} \rfloor\)
在\(k\)条边中枚举\(mid\)条边,全排列确定边的经过顺序,再枚举每条边的方向
把终点为\(end\),集合为\(S\)的答案存下来并取\(\min\),用于之后的拼接
设\(ans_{end,S}\)表示从\(1\)出发,经过了集合\(S\)里的边,最终停在\(end\)点上的最短路
拼接的时候枚举集合\(S\),并求出\(S\)的补集\(\complement S\)(因为\(S\)只是经过了一半边的答案)
枚举\(S\)的终点\(e_1\)和\(\complement S\)的终点\(e_2\),拼接后的答案就是\(ans_{e_1,S} + ans_{e_2,\complement S} + dis_{e1,e2}\),(\(dis_{e1,e2}\)表示\(e_1\)到\(e_2\)之间的最短路)
时间复杂度:跑最短路的部分是\(O((2k+1)(n + m) \log m)\),枚举+存答案的部分为\(O(2^{\frac{k}{2}} \cdot (\frac{k}{2})! \cdot 2^{\frac{k}{2}})\),拼接的复杂度为\(O(k^2 \cdot 2^{\frac{k}{2}})\),总复杂度就是它们加起来,时限3s还是没问题的
Warning:此代码对图中真正的点的编号和赋给关键点的新编号的处理十分混乱,可能会引起您的不适 码此代码的过程中也引起了我的不适,对此我感到十分抱歉
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define LL long long
using namespace std;
int read() {
int k = 0; char c = getchar();
while(c < '0' || c > '9') c = getchar();
while(c >= '0' && c <= '9')
k = k * 10 + c - 48, c= getchar();
return k;
}
struct zzz {
int t, nex; LL len;
}e[200010 << 1]; int head[50010], tot;
void add(int x, int y, LL z) {
e[++tot].t = y;
e[tot].len = z;
e[tot].nex = head[x];
head[x] = tot;
}
struct hhh {
LL v; int pos;
bool operator < (const hhh &y) const{
return v > y.v;
}
};
struct jjj {
int f, t; LL len;
}want[15];
LL dis[30][50010], cnt;
priority_queue <hhh> q;
void dijkstra(int s, int num) { //跑最短路
memset(dis[num], 127, sizeof(dis[num])); dis[num][s] = 0;
q.push((hhh){0, s});
while(!q.empty()) {
hhh k = q.top(); q.pop();
if(dis[num][k.pos] != k.v) continue;
for(int i = head[k.pos]; i; i = e[i].nex) {
int to = e[i].t;
if(dis[num][to] > dis[num][k.pos] + e[i].len) {
dis[num][to] = dis[num][k.pos] + e[i].len;
q.push((hhh){dis[num][to], to});
}
}
}
}
int n, m, k;
bool vis[50], viss[50];
int mid, tmp[5010], cn, a[50], node[50], ran[50010];
LL ans[30][1 << 15];
void meiju(int pos, int lim, int sti) { //枚举选出的边的方向
if(pos > lim) {
int from = 0; LL flag = 0;
for(int i = 1; i <= lim; ++i) {
if(viss[i]) {
flag += dis[from][want[a[i]].f] + want[a[i]].len;
from = ran[want[a[i]].t];
}
else {
flag += dis[from][want[a[i]].t] + want[a[i]].len;
from = ran[want[a[i]].f];
}
}
ans[from][sti] = min(flag, ans[from][sti]); //存答案
return ;
}
viss[pos] = 1;
meiju(pos+1, lim, sti);
viss[pos] = 0;
meiju(pos+1, lim, sti);
}
int main() {
memset(ans, 127/3, sizeof(ans));
n = read(), m = read(), k = read();
for(int i = 1; i <= m; ++i) {
int x = read(), y = read(), z = read();
add(x, y, z); add(y, x, z);
if(i <= k) want[i] = (jjj){x, y, z};
}
dijkstra(1, cnt);
if(k == 1) { //不知道什么原因,k = 1的时候会WA,那就特判掉好了
int x = want[1].f, y = want[1].t;
LL anss = dis[0][x] + want[1].len + dis[0][y];
printf("%lld\n", anss); return 0;
}
for(int i = 1; i <= k; ++i) { //预处理出最短路
node[++cnt] = want[i].f;
ran[want[i].f] = cnt;
dijkstra(want[i].f, cnt);
node[++cnt] = want[i].t;
ran[want[i].t] = cnt;
dijkstra(want[i].t, cnt);
}
mid = k >> 1;
for(int i = 0; i < (1 << k); ++i) { //预处理mid条边的集合
int x = i, y = 0;
while(x) {
if(x & 1) ++y;
x >>= 1;
}
if(y == mid) tmp[++cn] = i;
if((k & 1) && (y == mid+1)) tmp[++cn] = i;
}
for(int i = 1; i <= cn; ++i) { //枚举集合
memset(vis, 0, sizeof(vis));
int x = tmp[i], y = 0;
while(x) {
++y; if(x & 1) vis[y] = 1;
x >>= 1;
}
int p = 0;
for(int j = 1; j <= k; ++j)
if(vis[j]) a[++p] = j;
do {
memset(viss, 0, sizeof(viss));
meiju(1, p, tmp[i]); //确定边的方向
}while(next_permutation(a+1, a+p+1)); //全排列确定边的经过顺序
}
LL anss = 21474836477777777ll, S = (1 << k) - 1;
for(int i = 1; i <= cn; ++i) { //拼接
LL s1 = tmp[i], s2 = S ^ s1; //枚举集合并求补集
for(int l = 1; l <= cnt; ++l) { //枚举终点
for(int r = 1; r <= cnt; ++r) { //枚举终点++
int x = ran[node[l]], y = node[r];
anss = min(anss, ans[l][s1] + ans[r][s2] + dis[x][y]);
//因为不符合条件的情况得ans数组里会是一个极大值,所以可以不用特判(我比较懒)
}
}
}
cout << anss;
return 0;
}