NOIP 2017 宝藏
题目链接
https://www.luogu.org/problemnew/solution/P3959
这道题学到很多很多东西
还是很多收获的,虽然花了非常长的时间
首先直接写prim可以得45分,还是很多的
貌似不需要long long
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; typedef long long ll; const int MAXN = 20; int g[MAXN][MAXN]; int vis[MAXN], n, m; ll dis[MAXN], d[MAXN], ans; ll prim(int u) { memset(vis, 0, sizeof(vis)); vis[u] = 1; _for(i, 1, n) d[i] = i == u ? 0 : 1e9; _for(i, 1, n) dis[i] = i == u ? 0 : 1e9; _for(v, 1, n) if(g[u][v] != -1) { d[v] = min(d[v], (ll)g[u][v]); dis[v] = 1; } ll res = 0; REP(k, 1, n) { ll mint = 1e18; int v; _for(i, 1, n) if(!vis[i] && d[i] != 1e9 && mint > d[i]) mint = d[i], v = i; vis[v] = 1; res += mint; _for(i, 1, n) { if(vis[i] || g[v][i] == -1) continue; dis[i] = min(dis[i], dis[v] + 1); if(d[i] > g[v][i] * (dis[v] + 1)) { d[i] = g[v][i] * (dis[v] + 1); dis[i] = dis[v] + 1; } } } return res; } int main() { memset(g, -1, sizeof(g)); scanf("%d%d", &n, &m); _for(i, 1, m) { int u, v, w; scanf("%d%d%d", &u, &v, &w); if(g[u][v] == -1) g[u][v] = g[v][u] = w; else g[u][v] = g[v][u] = min(g[u][v], w); } ans = 1e18; _for(i, 1, n) ans = min(ans, prim(i)); printf("%lld\n", ans); return 0; }
然后直接打搜索竟然可以拿到70分!!
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; const int MAXN = 20; int g[MAXN][MAXN]; int vis[MAXN], n, m; int d[MAXN], ans; void dfs(int cnt, int now) { if(now >= ans) return; if(cnt == n) { ans = min(ans, now); return; } _for(i, 1, n) { if(!vis[i]) continue; _for(j, 1, n) if(!vis[j] && g[i][j] != -1) { d[j] = d[i] + 1; vis[j] = 1; dfs(cnt + 1, now + g[i][j] * d[j]); d[j] = vis[j] = 0; } } } int main() { memset(g, -1, sizeof(g)); scanf("%d%d", &n, &m); _for(i, 1, m) { int u, v, w; scanf("%d%d%d", &u, &v, &w); if(g[u][v] == -1) g[u][v] = g[v][u] = w; else g[u][v] = g[v][u] = min(g[u][v], w); } ans = 1e9; _for(i, 1, n) { vis[i] = 1; dfs(1, 0); vis[i] = 0; } printf("%d\n", ans); return 0; }
然后接下来要AC这道题就有很多方法了
有一个错误的状压可以AC,比搜索快在记忆化上
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; const int MAXN = 5000; int g[MAXN][MAXN]; int dp[MAXN], n, m; int d[MAXN]; void dfs(int S) { REP(i, 0, n) { if(!(S & (1 << i))) continue; REP(j, 0, n) { if(S & (1 << j) || g[i][j] == -1) continue; if(dp[S] + g[i][j] * d[i] < dp[S | (1 << j)]) { dp[S | (1 << j)] = dp[S] + g[i][j] * d[i]; d[j] = d[i] + 1; dfs(S | (1 << j)); } } } } int main() { memset(g, -1, sizeof(g)); scanf("%d%d", &n, &m); _for(i, 1, m) { int u, v, w; scanf("%d%d%d", &u, &v, &w); u--; v--; if(g[u][v] == -1) g[u][v] = g[v][u] = w; else g[u][v] = g[v][u] = min(g[u][v], w); } int ans = 1e9; REP(i, 0, n) { memset(dp, 0x3f, sizeof(dp)); memset(d, 0, sizeof(d)); d[i] = 1; dp[1 << i] = 0; dfs(1 << i); ans = min(ans, dp[(1 << n) - 1]); } printf("%d\n", ans); return 0; }
还有一个骗分的random_shuffle可以AC掉
因为这道题做一次贪心的时间非常非常小,所以可以随机化做非常非常多次的贪心,取最优
随机化的一个方法就是random_shuffle,重置排列
对于这道题可以拿这个排列作为开拓的顺序,然后贪心就好了。
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; const int MAXN = 20; int g[MAXN][MAXN]; int vis[MAXN], n, m; int d[MAXN], ans; void dfs(int cnt, int now) { if(now >= ans) return; if(cnt == n) { ans = min(ans, now); return; } _for(i, 1, n) { if(!vis[i]) continue; _for(j, 1, n) if(!vis[j] && g[i][j] != -1) { d[j] = d[i] + 1; vis[j] = 1; dfs(cnt + 1, now + g[i][j] * d[j]); d[j] = vis[j] = 0; } } } int main() { memset(g, -1, sizeof(g)); scanf("%d%d", &n, &m); _for(i, 1, m) { int u, v, w; scanf("%d%d%d", &u, &v, &w); if(g[u][v] == -1) g[u][v] = g[v][u] = w; else g[u][v] = g[v][u] = min(g[u][v], w); } ans = 1e9; _for(i, 1, n) { vis[i] = 1; dfs(1, 0); vis[i] = 0; } printf("%d\n", ans); return 0; }
然后是正解,比较难理解
据说这道题本来是T3??
思路网上都有,我讲几个我一直卡住的点
正解是按照深度一层一层来dp的
这个dp是不关注根在哪里的,只关注点的集合和深度
这点非常重要
首先不一定深度浅的优,因为边权不一样
然后这个深度只可能算大,不可能算小,因为转移的时候要用到一个cost数组(具体见程序)
如果算小的话说明这个集合不可能是这个深度,那么这个cost数组的值也就会非常大,表示不存在
所以顶多就是没有这么深但是你算得时候算这么深
不过这样不会漏掉最优解,因为你把所有的情况都枚举了一遍
还学到了许多技巧,如3^n枚举子集,用从小到大用lowbit作dp
虽然写了一天,但还是收获很大的。不求写题的数量,但求学到知识,有进步
#include<bits/stdc++.h> #define REP(i, a, b) for(register int i = (a); i < (b); i++) #define _for(i, a, b) for(register int i = (a); i <= (b); i++) using namespace std; const int MAXN = 13; const int MAXM = (1 << 12) + 10; int lg[MAXN], cost[MAXM][MAXM]; int g[MAXN][MAXN], dp[MAXN][MAXM]; int n, m; inline int lowbit(int x) { return x & (-x); } inline void down(int& a, int b) { a = min(a, b); } int main() { memset(g, -1, sizeof(g)); scanf("%d%d", &n, &m); _for(i, 1, m) { int u, v, w; scanf("%d%d%d", &u, &v, &w); u--; v--; if(g[u][v] == -1) g[u][v] = g[v][u] = w; else g[u][v] = g[v][u] = min(g[u][v], w); } int Smax = (1 << n) - 1; REP(i, 0, n) lg[1 << i] = i; REP(S, 1, 1 << n) { int T = Smax ^ S; //表示补集 stack<int> st; for(int S0 = T; S0; S0 = (S0-1) & T) st.push(S0); //S0从小到大枚举,这样dp顺序才对 while(!st.empty()) { int S0 = st.top(); st.pop(); int u = lg[lowbit(S0)], mint = 5e5 + 10; //如果这个mint要加很多次的话,就按题目的最大来 REP(v, 0, n) //不要省事写1e9,否则40分没了!! if(S & (1 << v) && g[u][v] != -1) // 这里只转移lowbit这个点就好了,之前的最小值已经转移过了 down(mint, g[u][v]); cost[S][S0] = cost[S][S0 ^ (1 << u)] + mint; //注意这里是S转移到S0,通过S0中的点向S连边 } } memset(dp, 0x3f, sizeof(dp)); REP(i, 0, n) dp[0][1 << i] = 0; _for(h, 1, n) REP(S, 1, 1 << n) for(int S0 = S; S0; S0 = (S0 - 1) & S) down(dp[h][S], dp[h-1][S ^ S0] + cost[S ^ S0][S0] * h); int ans = 1e9; _for(h, 0, n) //注意这里深度可能为0,即一个点没有边,很坑 down(ans, dp[h][Smax]); printf("%d\n", ans); return 0; }