T1
洛谷 P2047 社交網路
暴力 Floyd 跑出每兩個點間最短路及其條數,然後暴力列舉算。
程式碼
#include <iostream>
#include <string.h>
#include <iomanip>
#include <queue>
#define int long long
using namespace std;
int n, m;
int dist[105][105];
int cnt[105][105];
signed main() {
cin >> n >> m;
memset(dist, 63, sizeof dist);
for (int i = 1; i <= m; i++) {
int u, v, ww;
cin >> u >> v >> ww;
dist[u][v] = min(dist[u][v], ww);
dist[v][u] = dist[u][v];
cnt[u][v] = cnt[v][u] = 1;
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
cnt[i][j] = cnt[i][k] * cnt[k][j];
} else if (dist[i][k] + dist[k][j] == dist[i][j])
cnt[i][j] += cnt[i][k] * cnt[k][j];
}
}
}
for (int i = 1; i <= n; i++) {
double ans = 0;
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
if (i == j || i == k || j == k)
continue;
if (dist[j][i] + dist[i][k] == dist[j][k])
ans += 1.0 * cnt[j][i] * cnt[i][k] / cnt[j][k];
}
}
cout << fixed << setprecision(3) << ans << "\n";
}
return 0;
}
T2
洛谷 P4189 星際旅行
首先題目保證每個點的值大於等於其度數,所以可以先把每個點遍歷一遍。接下來如果有一條邊兩邊的點剩下的都大於 \(0\),那就可以在走到這兩個點時在這兩點間反覆橫跳來把其中一個消耗成 \(0\)。這樣可以求得根的答案。接下來考慮往下推。假設我們已經知道了這個點的答案,要求其中一個兒子的答案。這個時候分類討論。
-
當前點有剩餘。直接走,兒子答案為當前點答案加 \(1\)。
-
當前點無剩餘,但是兒子有至少一個兒子還有剩餘。那就把從兒子上來的一條流退掉,換成從兒子到兒子的兒子再回兒子的一條流。這樣兒子答案是當前點答案 \(+1\),兒子的兒子剩餘值 \(-1\)。
-
否則退掉從兒子上來的一條流,答案為當前點答案 \(-1\),兒子剩餘值 \(+1\)。
直接搞就行了。回溯時要記得把所有對剩餘值的更改清掉。
程式碼
#include <iostream>
using namespace std;
int n;
int h[50005];
int deg[50005];
int head[50005], nxt[100005], to[100005], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt, deg[v]++; }
int f[50005];
int ans[50005];
int p[50005];
int cur;
void dfs1(int x, int fa) {
f[x] = fa;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != fa) {
dfs1(v, x);
int tmp = min(h[x], h[v]);
ans[1] += tmp * 2;
h[x] -= tmp;
h[v] -= tmp;
if (h[v])
p[x] = v;
}
}
}
void dfs2(int x) {
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (v != f[x]) {
if (h[x]) {
ans[v] = ans[x] + 1, --h[x];
dfs2(v);
++h[x];
} else if (p[v]) {
ans[v] = ans[x] + 1, h[p[v]]--;
dfs2(v);
h[p[v]]++;
} else {
ans[v] = ans[x] - 1, h[v]++;
dfs2(v);
h[v]--;
}
}
}
}
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
cin >> n;
for (int i = 1; i <= n; i++) cin >> h[i];
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
++u, ++v;
add(u, v);
add(v, u);
ans[1] += 2;
h[u]--, h[v]--;
}
dfs1(1, 0);
dfs2(1);
for (int i = 1; i <= n; i++) cout << ans[i] << "\n";
return 0;
}
T3
洛谷 P1963 變換序列
把一個點向能變到的點連邊,構成一張二分圖。則問題變為求一張二分圖的字典序最小的完美匹配,且每個左部點的度數都為 \(2\)。使用匈牙利演算法,倒著列舉每個點,對於每個點儘量選更小的匹配點。可以證明這樣匹配出來就是字典序最小。
證明考慮先刪去所有度數為 \(1\) 的右部點。設還剩 \(k\) 個右部點,則右部點總度數還剩 \(2n - 2(n - k) = 2k\)。而每個右部點的度數都大於等於 \(2\),所以每個右部點的度數都只能為 \(2\)。又因為所有左部點度數也都為 \(2\),所以每次換掉一個點的匹配時就只有一種剩下的情況。所以就能保證字典序最小。
程式碼
#include <iostream>
#include <algorithm>
#include <string.h>
#include <vector>
using namespace std;
int n, S, T;
int d[10005];
vector<int> g[100005];
void add(int u, int v) {
g[u].emplace_back(v);
g[v].emplace_back(u);
}
int mat[2000005];
bool vis[2000005];
bool dfs(int x) {
if (vis[x])
return 0;
vis[x] = 1;
for (auto v : g[x]) {
if (!mat[v] || dfs(mat[v])) {
mat[v] = x;
mat[x] = v;
return 1;
}
}
return 0;
}
signed main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
cin >> n;
S = n * 2 + 1, T = S + 1;
for (int i = 1; i <= n; i++) cin >> d[i];
for (int i = n; i; i--) {
int x = ((i - 1 - d[i]) + n) % n + 1;
int y = (i - 1 + d[i]) % n + 1;
if (x < y)
swap(x, y);
add(i, x + n);
add(i, y + n);
}
for (int i = 1; i <= n; i++) sort(g[i].begin(), g[i].end());
int x = 0;
for (int i = n; i; i--) {
memset(vis, 0, sizeof vis);
x += dfs(i);
}
if (x != n)
cout << "No Answer\n";
else {
for (int i = 1; i <= n; i++)
cout << mat[i] - n - 1 << " ";
}
return 0;
}
T4
洛谷 P1912 詩人小 G
首先有顯然的狀態與轉移方程:\(dp[i] = \min\limits_{0 \le j < i} \{ dp[j] + |S[i] - S[j] + j - i - 1 - L|^p \}\)。可以發現這個轉移具有決策單調性,所以利用決策單調性來最佳化 dp。
決策單調性最佳化 dp:
考慮每個點一開始的最優決策:\(\texttt{0000000}\)。在有了 \(dp[1]\) 之後,最優決策可能變成這樣:\(\texttt{0001111}\)。然後可能是 \(\texttt{0001122}\),\(\texttt{0001333}\),以此類推。發現以每個決策為最優決策點的點是一段區間,所以我們用一個佇列維護所有這樣的區間,其中存若干三元組,分別表示某個決策的左右端點以及這個決策的位置。每次新加入一個決策 \(i\) 時,從右往左遍歷這個佇列。設佇列最右側的決策左端點為 \(l\),決策點為 \(p\),那如果 \(l\) 用 \(i\) 這個決策比 \(l\) 用 \(p\) 這個決策來的更優,那 \(p\) 這個決策就沒有用了,可以直接扔掉。所以此時我們彈掉佇列的右端點,直到 \(l\) 用 \(p\) 更優。此時由於 \(i\) 的加入,原本 \(p\) 的區間裡可能出現一些用 \(i\) 更優的位置。這些位置一定是右邊的一串,所以我們二分出第一個用 \(i\) 更優的位置 \(x\),將 \(p\) 的區間的右端點設為 \(x - 1\),然後加入新的決策 \(\{ x, n, i\}\)。然後我們再把佇列最左邊的決策的左端點設為 \(i + 1\)。這樣每次算新的 dp 值時就把最左邊的合法決策拿出來算一下就好了。記得扔掉左端點大於右端點的決策。
然後就是這個題 dp 要開 long double 存,不然要爆精度。
程式碼
#include <iostream>
#include <iomanip>
#include <math.h>
#define int long long
using namespace std;
const long long inf = 1000000000000000000ll;
int n, L, p;
inline long double qpow(long double x, int y) {
long double ret = 1;
while (y) {
if (y & 1)
ret = ret * x;
y >>= 1;
x = x * x;
}
return ret;
}
int S[100005];
long double dp[100005];
inline long double calc(int i, int j) { return dp[j] + qpow(abs(S[i] - S[j] + i - j - 1 - L), p); }
struct node {
int l, r, p;
} q[100005];
int ql, qr;
int Search(int ll, int rr, int x, int y) {
int l = ll, r = rr, mid, ans = rr + 1;
while (l <= r) {
mid = (l + r) >> 1;
if (calc(mid, x) <= calc(mid, y)) {
ans = mid, r = mid - 1;
} else {
l = mid + 1;
}
}
return ans;
}
string str[100005];
int f[100005];
int stk[100005], sz;
signed main() {
int tc;
cin >> tc;
while (tc--) {
cin >> n >> L >> p;
for (int i = 1; i <= n; i++) {
cin >> str[i];
S[i] = S[i - 1] + str[i].size();
}
ql = qr = 1;
q[qr] = (node) { 1, n, 0 };
for (int i = 1; i <= n; i++) {
while (q[ql].l > q[ql].r) ++ql;
dp[i] = calc(i, q[ql].p);
f[i] = q[ql].p;
while (q[qr].r < q[qr].l || (qr >= ql && calc(q[qr].l, i) <= calc(q[qr].l, q[qr].p))) --qr;
if (ql > qr)
q[++qr] = (node) { i, n, i };
else {
int x = Search(q[qr].l, q[qr].r, i, q[qr].p);
q[qr].r = x - 1;
q[++qr] = (node) { x, n, i };
}
q[ql].l = i + 1;
}
if (dp[n] > inf)
cout << "Too hard to arrange\n";
else {
cout << fixed << setprecision(0) << dp[n] << "\n";
sz = 0;
for (int i = n; i; i = f[i]) stk[++sz] = i;
stk[sz + 1] = 0;
for (int i = sz; i; i--) {
int p = stk[i + 1];
for (int j = p + 1; j <= stk[i]; j++) cout << str[j] << " \n"[j == stk[i]];
}
}
for (int i = 1; i <= 20; i++) cout << "-";
cout << "\n";
}
return 0;
}
T5
洛谷 P2805 植物大戰殭屍
首先把所有植物向它所保護的植物連邊,再向它左邊的植物連邊,表示要先吃掉這個植物才能吃掉那個。這樣建出的圖可能會存在環,而我們發現所有環能夠到達的點都無法被吃掉。所以我們先拓撲一遍,能拓撲到的點都是無法被環所到達的。接下來我們把所有邊反向,發現選了一個植物之後就要選所有其能夠到達的。問題轉化為求最大權閉合子圖。
最大權閉合子圖:
圖中原有邊容量為 \(+\infty\),從源點向所有正權點連邊,邊權為該點的點權;從所有負權點向匯點連邊,邊權為該點點權的絕對值。然後求 所有正權點點權和 - 最小割 即為答案。首先所有原圖中的邊不會被割掉,所以所有被劃到 \(S\) 集合的點能夠到達的所有點一定也在 \(S\) 集合中。這樣就保證了是閉合的。然後我們考慮一組割的意義。首先假設所有正權點都在 \(S\) 中。如果我們割掉一條正權點的邊,那代表不把這個正權點劃到閉合子圖中,子圖權值要減去其權值。如果我們割掉一條負權點的邊,那代表把這個點劃到閉合子圖中,子圖權值要加上其權值,也就是減去負的它的權值。這樣我們求出最小割然後減一下就可以了。
程式碼
#include <iostream>
#include <vector>
#include <queue>
#define int long long
using namespace std;
const int inf = 2147483647;
int n, m, N;
int sc[605];
int in[605];
vector<int> g[605];
queue<int> q;
bool vis[605];
inline int f(int x, int y) { return (x - 1) * m + y; }
int pcnt[605];
int head[605], nxt[2000005], to[2000005], res[2000005], ecnt;
int cur[605];
void add(int u, int v, int ww) {
to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt, res[ecnt] = ww;
to[++ecnt] = u, nxt[ecnt] = head[v], head[v] = ecnt, res[ecnt] = 0;
}
int S, T;
int dep[605];
bool bfs() {
q.push(S);
for (int i = 1; i <= T; i++) dep[i] = -1;
dep[S] = 1;
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (res[i] && dep[v] == -1) {
dep[v] = dep[x] + 1;
q.push(v);
}
}
}
return (dep[T] != -1);
}
int dfs(int x, int flow) {
if (x == T)
return flow;
int ret = 0;
for (int i = cur[x]; i && flow; i = nxt[i]) {
cur[x] = i;
int v = to[i];
if (dep[v] == dep[x] + 1 && res[i]) {
int tmp = dfs(v, min(flow, res[i]));
if (tmp) {
res[i] -= tmp;
res[i ^ 1] += tmp;
flow -= tmp;
ret += tmp;
}
}
}
if (ret == 0)
dep[x] = -1;
return ret;
}
int dinic() {
int ret = 0;
while (bfs()) {
for (int i = 1; i <= T; i++) cur[i] = head[i];
ret += dfs(S, inf);
}
return ret;
}
signed main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
cin >> n >> m;
N = n * m;
S = N + 1, T = S + 1;
ecnt = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int x = f(i, j);
cin >> sc[x];
cin >> pcnt[x];
for (int k = 1; k <= pcnt[x]; k++) {
int a, b;
cin >> a >> b;
++a, ++b;
g[x].emplace_back(f(a, b));
in[f(a, b)]++;
}
if (j ^ 1) {
g[x].emplace_back(f(i, j - 1));
in[f(i, j - 1)]++;
}
}
}
for (int i = 1; i <= N; i++) {
if (!in[i])
q.push(i);
}
while (!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 1;
for (auto v : g[x]) {
in[v]--;
if (in[v] == 0)
q.push(v);
}
}
int s = 0;
for (int i = 1; i <= N; i++) {
if (!vis[i])
continue;
if (sc[i] < 0)
add(i, T, -sc[i]);
else
add(S, i, sc[i]), s += sc[i];
for (auto v : g[i]) {
if (vis[v])
add(v, i, inf);
}
}
cout << s - dinic() << "\n";
return 0;
}
T6
洛谷 P1758 管道取珠
由於是對平方求和,考慮變成兩個人分別取,取出來的序列相同的方案數。這樣就隨便 dp 了。設 \(dp[i][j][k]\) 表示兩個人目前都取了 \(i\) 個,一個人在第一管裡取了 \(j\) 個,第二個在第一管裡取了 \(k\) 個,目前取出來的序列相同的方案數。第一維滾動陣列滾掉,然後轉移隨便搞搞就好。
程式碼
#include <iostream>
#include <algorithm>
#include <string.h>
#define int long long
using namespace std;
const int P = 1024523;
int n, m;
int dp[2][505][505];
string s, t;
signed main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
cin >> n >> m;
cin >> s >> t;
s = ' ' + s;
t = ' ' + t;
dp[0][0][0] = 1;
for (int i = 1; i <= n + m; i++) {
int x = (i & 1);
memset(dp[x], 0, sizeof dp[x]);
for (int j = max(0ll, i - m); j <= min(n, i); j++) {
for (int k = max(0ll, i - m); k <= min(n, i); k++) {
if (s[j] == s[k] && j && k)
dp[x][j][k] += dp[x ^ 1][j - 1][k - 1];
if (s[j] == t[i - k] && j && i - k)
dp[x][j][k] += dp[x ^ 1][j - 1][k];
if (t[i - j] == s[k] && i - j && k)
dp[x][j][k] += dp[x ^ 1][j][k - 1];
if (t[i - j] == t[i - k] && i - j && i - k)
dp[x][j][k] += dp[x ^ 1][j][k];
dp[x][j][k] %= P;
}
}
}
cout << dp[(n + m) & 1][n][n] << "\n";
return 0;
}
T7
洛谷 P2046 海拔
首先最終的答案一定是一堆海拔為 \(0\) 的構成且僅構成一個四連通塊,一堆海拔為 \(1\) 的也構成且僅構成一個四連通塊。因為如果有一個內部高度相等四連通塊比與它四聯通的所有點海拔都高,那一定可以把這整個四連通塊的高度降低而使答案更優。這樣就轉化為一個最小割。然後又由於是平面圖,我們使用平面圖最小割等於對偶圖最短路這個結論。也就是在源匯點間連邊,構成一個附加面。然後構造出對偶圖,但是不連附加面與開放面之間的邊。然後我們跑出附加面對應的點到開放面對應的點之間的最短路即為最小割。證明考慮新圖中一條路徑就對應一組割,所以最短路就是最小割。畫個圖大概就能理解。
程式碼
#include <iostream>
#include <string.h>
#include <queue>
using namespace std;
int S, T;
int head[250005], nxt[4000005], to[4000005], ew[4000005], ecnt;
void add(int u, int v, int ww) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt, ew[ecnt] = ww; }
int dist[250005];
struct node {
int x, dis;
} tmp;
bool operator<(node a, node b) { return a.dis > b.dis; }
priority_queue<node> q;
bool vis[250005];
void dijkstra() {
memset(dist, 63, sizeof dist);
q.push((node) { S, 0 });
dist[S] = 0;
while (!q.empty()) {
tmp = q.top();
q.pop();
int x = tmp.x;
if (vis[x])
continue;
vis[x] = 1;
for (int i = head[x]; i; i = nxt[i]) {
int v = to[i];
if (dist[v] > dist[x] + ew[i]) {
dist[v] = dist[x] + ew[i];
q.push((node) { v, dist[v] });
}
}
}
}
int n;
inline int f(int x, int y) { return (x - 1) * n + y; }
int main() {
// freopen("data.in", "r", stdin);
// freopen("data.out", "w", stdout);
cin >> n;
S = n * n + 1, T = S + 1;
for (int i = 1; i <= n + 1; i++) {
for (int j = 1; j <= n; j++) {
int x;
cin >> x;
if (i == 1)
add(f(1, j), T, x);
else if (i == n + 1)
add(S, f(n, j), x);
else
add(f(i, j), f(i - 1, j), x);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n + 1; j++) {
int x;
cin >> x;
if (j == 1)
add(S, f(i, 1), x);
else if (j == n + 1)
add(f(i, n), T, x);
else
add(f(i, j - 1), f(i, j), x);
}
}
for (int i = 1; i <= n + 1; i++) {
for (int j = 1; j <= n; j++) {
int x;
cin >> x;
if (i == 1)
add(T, f(1, j), x);
else if (i == n + 1)
add(f(n, j), S, x);
else
add(f(i - 1, j), f(i, j), x);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n + 1; j++) {
int x;
cin >> x;
if (j == 1)
add(f(i, 1), S, x);
else if (j == n + 1)
add(T, f(i, n), x);
else
add(f(i, j), f(i, j - 1), x);
}
}
dijkstra();
cout << dist[T] << "\n";
return 0;
}