开始有时间改正NOIp2017的题目了,在这篇文章中,简单写一下题解。题意和我在比赛时的原始代码见游记 。
小凯的疑惑(math)
没什么可说的了,以后再证明吧。
math.cpp 1 2 3 4 5 6 7 8 9 10 11 #include <fstream> using namespace std;ifstream fin ("math.in" ) ;ofstream fout ("math.out" ) ;int main () { int a, b; fin >> a >> b; fout << 1ll * a * b - a - b << endl; return 0 ; }
时间复杂度(complexity)
参见游记。
逛公园(park)
很多人都会做这题,然而我并不会,参考了洛谷的题解。这就是day1的DP,不过比较简单。对于SSSP,我偏好Dijkstra而不是SPFA,因为我出过卡SPFA的题。不过这题并不卡SPFA。
$K=0$
$K=0$时就是$1->N$的最短路计数,与最优化问题DP计数一样。设$d[u]$为$1->u$的最短路,$dp[u]$表示$1->u$的最短路数,则有
$$dp[1]=1$$
$$dp[v]=\sum_{u,v\in E}dp[u],d[v]=d[u]+w_{u,v}$$
无0权边
因为$K\le50$,可以设计$dp[u][k]$表示$1->u$比$d$多走了$k$的路径数,则有
$$dp[u][k]=\begin{cases}1 & u=n \ 0&u\ne n\end{cases}+\sum_{u,v\in E}dp[v][d[u]+k+w_{u,v}-d[v]]$$
注意到达$N$后可能可以绕一圈再回去。
有0权边
显然,如果在$d+K$内经过一个0权环,就有无穷多条路径,只要在DP时发现有重复直接返回即可。
然而这样复杂度并不对,DP可能是$\mathcal O(NMK)$的。需要反向最短路$rd$,当$d[u]+k+w_{u,v}+rd[v]>d+K$时剪枝。这样时间复杂度应该为$\mathcal O(M\log M+MK)$。
park.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 #include <fstream> #include <queue> #include <algorithm> #include <functional> using namespace std;ifstream fin ("park.in" ) ;ofstream fout ("park.out" ) ;const int N = 100005 , M = 200005 , K = 51 , INF = 0x3f3f3f3f ;int n, m, k, mod, f[N][K];typedef pair<int , int > state;struct graph { int head[N], v[M], w[M], nxt[M], e, d[N]; bool vis[N]; void reset () { fill (head + 1 , head + n + 1 , 0 ); e = 0 ; } void insert (int u, int v, int w) { graph::v[++e] = v; graph::w[e] = w; nxt[e] = head[u]; head[u] = e; } void dijkstra (int s) { fill (vis + 1 , vis + n + 1 , false ); fill (d + 1 , d + n + 1 , INF); d[s] = 0 ; priority_queue<state, vector<state>, greater<state> > Q; Q.push (make_pair (0 , s)); while (!Q.empty ()) { state k = Q.top (); Q.pop (); if (vis[k.second]) continue ; vis[k.second] = true ; for (int i = head[k.second]; i; i = nxt[i]) if (d[k.second] + w[i] < d[v[i]]) Q.push (make_pair (d[v[i]] = d[k.second] + w[i], v[i])); } } } mat, rmat; bool vis[N][K];int dp (int u, int k) { if (vis[u][k]) return INF; if (~f[u][k]) return f[u][k]; vis[u][k] = true ; f[u][k] = u == n; for (int i = mat.head[u]; i; i = mat.nxt[i]) { int nk = mat.d[u] + k + mat.w[i] - mat.d[mat.v[i]]; if (nk <= ::k && mat.d[u] + k + mat.w[i] + rmat.d[mat.v[i]] <= mat.d[n] + ::k) { int ret = dp (mat.v[i], nk); if (ret == INF) return INF; (f[u][k] += ret) %= mod; } } vis[u][k] = false ; return f[u][k]; } int main () { int t; fin >> t; while (t--) { fin >> n >> m >> k >> mod; mat.reset (); rmat.reset (); while (m--) { int u, v, w; fin >> u >> v >> w; mat.insert (u, v, w); rmat.insert (v, u, w); } mat.dijkstra (1 ); rmat.dijkstra (n); fill_n (&f[0 ][0 ], sizeof (f) / sizeof (int ), -1 ); fill_n (&vis[0 ][0 ], sizeof (vis) / sizeof (bool ), false ); int ans = dp (1 , 0 ); if (ans == INF) fout << -1 << endl; else fout << ans << endl; } return 0 ; }
std::vector 被卡常我也无语了,比赛尤其不要用STL。
奶酪(cheese)
修改了判断条件,可以通过原来的数据范围。
cheese.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <fstream> #include <queue> #include <algorithm> using namespace std;ifstream fin ("cheese.in" ) ;ofstream fout ("cheese.out" ) ;const int N = 1005 ;int x[N], y[N], z[N];bool vis[N];inline long long sqr (long long x) { return x * x; } int main () { int t; fin >> t; while (t--) { int n, h, r; fin >> n >> h >> r; queue<int > Q; for (int i = 1 ; i <= n; i++) { fin >> x[i] >> y[i] >> z[i]; if (abs (z[i]) <= r) { vis[i] = true ; Q.push (i); } else vis[i] = false ; } while (!Q.empty ()) { int k = Q.front (); Q.pop (); for (int j = 1 ; j <= n; j++) if (!vis[j] && sqr (x[k] - x[j]) + sqr (y[k] - y[j]) <= 4 * sqr (r) - sqr (z[k] - z[j])) { vis[j] = true ; Q.push (j); } } bool ans = false ; for (int i = 1 ; i <= n; i++) if (vis[i] && abs (z[i] - h) <= r) { ans = true ; break ; } if (ans) fout << "Yes\n" ; else fout << "No\n" ; } return 0 ; }
宝藏(treasure)
错误但好写的状压
突然发现比赛时的想法只要记忆化一下就可以变成状压DP了。原来的d[]数组其实就是二进制状态,转移时直接暴力复制就可以了。
具体的,令$dist[u][v]$为$u->v$的边权,$dp[i][S],i\in S$表示以$i$为根节点,可到达的点集为$S$的最小花费,$d[i][S][u],i\in S$表示在最小花费下$i->u$的边数 ,则有
$$dp[i][{i}]=0$$
$$dp[i][S+v]=dp[i][S]+(d[i][S][u]+1)\times dist[u][v],u\in S,u,v\in E$$
其中$d$可以在DP转移时同时$\mathcal O(n)$转移。时间复杂度$\mathcal O(2^n n^4)$,比我知道的 最优的$\mathcal O(2^n n^3)$差一点,但也不错了。空间复杂度$\mathcal O(2^n n)$。
treasure.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <fstream> #include <algorithm> using namespace std;ifstream fin ("treasure.in" ) ;ofstream fout ("treasure.out" ) ;const int N = 12 , INF = 1e9 ;int mat[N][N], f[1 << N], d[1 << N][N];int main () { int n, m; fin >> n >> m; while (m--) { int u, v, w; fin >> u >> v >> w; u--; v--; if (!mat[u][v] || w < mat[u][v]) mat[u][v] = mat[v][u] = w; } int ans = INF; for (int i = 0 ; i < n; i++) { fill (f, f + (1 << n), INF); f[1 << i] = 0 ; for (int j = 0 ; j < n; j++) d[1 << i][j] = i == j ? 0 : INF; for (int j = 0 ; j < (1 << n) - 1 ; j++) for (int k = 0 ; k < n; k++) if (j & (1 << k)) for (int v = 0 ; v < n; v++) if (mat[k][v] && d[j][v] == INF && f[j] + (d[j][k] + 1 ) * mat[k][v] < f[j | (1 << v)]) { f[j | (1 << v)] = f[j] + (d[j][k] + 1 ) * mat[k][v]; for (int u = 0 ; u < n; u++) d[j | (1 << v)][u] = u == v ? d[j][k] + 1 : d[j][u]; } ans = min (ans, f[(1 << n) - 1 ]); } fout << ans << endl; return 0 ; }
更新 :但其实这样有问题。当dp的花费最优时,保存的距离d[]不一定最优,对拍已经发现反例了。然而比赛时的dfs是正确的,因为有回溯,事实上我就是用这个作为std的。可以看一下能不能卡掉一些人。
1 2 3 4 5 6 7 8 9 10 6 9 5 4 5 3 6 1 1 5 4 5 3 4 1 6 2 5 6 5 2 1 1 1 3 2 2 6 2
正确但很慢的状压
暂时不引入,较为复杂,复杂度可达$\mathcal O(3^n)$级别。
随机化
可以使用随机化水过,比较高端的是模拟退火,而低端的是直接random_shuffle
然后贪心,足以水过没有针对性的数据。然而,上述数据依然被卡掉。而洛谷上可以找到优秀的模拟退火,而且不会被卡掉。
treasure_rand.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <iostream> #include <random> #include <algorithm> using namespace std;const int N = 15 , INF = 1e9 ;int mat[N][N], p[N], d[N];int main () { minstd_rand gen (19260817 ) ; int n, m; cin >> n >> m; fill_n (&mat[0 ][0 ], sizeof (mat) / sizeof (int ), INF); while (m--) { int u, v, w; cin >> u >> v >> w; if (w < mat[u][v]) mat[u][v] = mat[v][u] = w; } for (int i = 1 ; i <= n; i++) p[i] = i; int ans = INF; for (int t = 1 ; t <= 10000 ; t++) { shuffle (p + 1 , p + n + 1 , gen); d[p[1 ]] = 1 ; int now = 0 ; for (int i = 2 ; i <= n; i++) { int j = 0 ; for (int k = 1 ; k < i; k++) if (!j || mat[p[k]][p[i]] < mat[p[j]][p[i]]) j = k; if (!j || mat[p[j]][p[i]] == INF) { now = INF; break ; } now += mat[p[j]][p[i]] * d[p[j]]; d[p[i]] = d[p[j]] + 1 ; } ans = min (ans, now); } cout << ans << endl; return 0 ; }
搜索剪枝
其实也挺难写的……
队列(phalanx)
其实比赛时的思路已经很接近正解了,只是我懒得想也不敢写。$n$行都维护一颗线段树,还有最后一列。一个问题是空间,但只要动态开点线段树,单次操作空间复杂度就是$\log n$级别的,完全可以。另一个问题是初始的编号,这也是比赛时我认为不可做的主要原因,现在想起来其实只要根据坐标就可以算出初始编号,思想与50%的数据类似。接下来只剩下一些细节问题,我调了不少时间,代码可能比较繁杂。事实证明,线段树并不会被卡常,至少在本地和洛谷。
时间复杂度$\mathcal O(q\log (n+q))$,空间复杂度相同。
phalanx.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <fstream> #include <vector> #include <algorithm> using namespace std;ifstream fin ("phalanx.in" ) ;ofstream fout ("phalanx.out" ) ;const int N = 300005 , L = 12e6 ;struct node { int sum, ls, rs; } tree[L]; int cc, root[N], last;vector<long long > mat[N], ml; int query (int id, int l, int r, int x) { if (l == r) return l; int mid = (l + r) / 2 ; if (mid - tree[tree[id].ls].sum >= x) return query (tree[id].ls, l, mid, x); return query (tree[id].rs, mid + 1 , r, x + tree[tree[id].ls].sum); } void modify (int &id, int l, int r, int x) { if (!id) id = ++cc; if (l == r) tree[id].sum++; else { int mid = (l + r) / 2 ; if (x <= mid) modify (tree[id].ls, l, mid, x); else modify (tree[id].rs, mid + 1 , r, x); tree[id].sum = tree[tree[id].ls].sum + tree[tree[id].rs].sum; } } int main () { int n, m, q; fin >> n >> m >> q; for (int i = 1 ; i <= n; i++) mat[i].push_back (1ll * i * m); ml.push_back (1ll * n * m); int r = max (n, m) + q; while (q--) { int x, y; fin >> x >> y; long long tmp = query (last, 1 , r, x); if (tmp <= n) tmp = 1ll * tmp * m; else tmp = ml[tmp - n]; if (m + mat[x].size () - 1 - tree[root[x]].sum == m) modify (root[x], 1 , r, m + mat[x].size () - 1 ); mat[x].push_back (tmp); long long num = query (root[x], 1 , r, y); modify (root[x], 1 , r, num); if (num <= m) num = 1ll * (x - 1 ) * m + num; else num = mat[x][num - m]; modify (last, 1 , r, query (last, 1 , r, x)); ml.push_back (num); fout << num << '\n' ; } return 0 ; }
终于断断续续的写完了这篇文章,从12/2创建到12/26完工。