QOJ 3276 出題高手
注意到資料隨機,這提示我們有用的區間數並不多,考慮將這些區間算出來,計算一個區間對一次詢問的貢獻。
Solution 1
做字首和 \(s_i = \sum\limits_{k = 1}^i a_i\),考慮一對 \((i, j)\) 的價值是 \(\frac{(s_j - s_i)^2}{j - i}\)(\(0 \le i < j \le n\)),我們先考慮 \(s_j > s_i\) 的情況(\(s_j \le s_i\) 類似),此時固定一個 \(j\),考慮兩個決策點 \(i_0, i_1\),若 \(i_0 > i_1\) 且 \(s_{i_0} < s_{i_1}\),則 \(i_0\) 一定優於 \(i_1\),那麼有用的 \(i\) 的數量是隨機序列字首和的單調棧長度,是 \(O(\sqrt n)\) 的,那麼總區間數是 \(O(n \sqrt n)\),無法接受。
Solution 2
前面的做法總區間數過多,其實是因為只考慮的左端點的限制,沒有考慮右端點的限制。考慮 \(s_j > s_i\) 的情況,我們注意一對 \((i, j)\) 有用的必要條件是 \(\forall i \le i_0 < j_0 \le j\),\(s_j - s_i > s_{j_0} - s_{i_0}\),那麼其實我們要求 \(\forall k \in [i + 1, j - 1], s_i < s_k < s_j\),考慮若存在 \(s_k \le s_i\),將 \(i\) 移到 \(k\) 一定變優,若存在 \(s_k \ge s_j\),將 \(j\) 移到 \(k\) 一定變優。\(s_j \le s_i\) 類似。
設 \((i, j)\) 的數量為 \(cnt\),考慮 \(s_i < s_k < s_j\) 的限制用單調棧即可維護,使得每對 \(i, j\) 只被算 \(1\) 次,時間複雜度 \(O(cnt)\)。設詢問區間為 \([l, r]\),\((i, j)\) 對其有貢獻當且僅當 \(l \le i + 1 \le j \le r\),樹狀陣列維護即可,時間複雜度 \(O(cnt \log n)\)。
考慮 Solution 1 只考慮左端點的限制,將 \(O(n^2)\) 的區間數縮小到 \(O(n \sqrt n)\),相當於將問題規模指數級縮小了 \(\frac{3}{4}\)(\(x \gets x^{\frac{3}{4}}\))。由於資料隨機,考慮一個端點的限制可以認為是隨機排除一些區間,所以是做兩遍 \(x \gets x^{\frac{3}{4}}\),故在 Solution 2 中,\(cnt = O(n^{\frac{9}{8}})\)。這些分析已經過打表證明。
時間複雜度 \(O((n^{\frac{9}{8}} + m) \log n)\)。對於 \(\text{Subtask2, 3}\) 需要特判,時間複雜度 \(O(n^{\frac{9}{8}})\)。
Code
#include <iostream>
#include <algorithm>
#include <stack>
#include <vector>
#include <chrono>
#include <random>
using namespace std;
mt19937 R(chrono::steady_clock::now().time_since_epoch().count());
int Rand (int l, int r) {
return uniform_int_distribution<int>(l, r)(R);
}
using LL = long long;
struct Frac {
LL x, y;
Frac () : x(0), y(1) {}
Frac (LL _x, LL _y) : x(_x), y(_y) {
LL g = __gcd(x, y);
x /= g, y /= g;
}
bool operator< (Frac a) const { return (__int128)x * a.y < (__int128)a.x * y; }
void Print () { cout << x << ' ' << y << '\n'; }
};
struct Node {
int o, l, r, v;
};
const int N = 2e3 + 5;
int n, m;
int a[N], s[N], Rles[N], Rgre[N], tr[N], ans[N][N];
vector<Frac> fvec;
vector<Node> arr;
void Init (int o, int *ret) {
stack<int> st;
fill(ret + 1, ret + n + 1, n + 1);
for (int i = 1; i <= n; ++i) {
while (!st.empty() && (!o ? s[i] < s[st.top()] : s[i] > s[st.top()])) {
ret[st.top()] = i;
st.pop();
}
st.push(i);
}
}
void Insert (int x, int y) {
for (; x <= n; x += (x & -x)) {
tr[x] = max(tr[x], y);
}
}
int Query (int x) {
int res = 0;
for (; x; x -= (x & -x)) {
res = max(res, tr[x]);
}
return res;
}
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
if (n > 2000) return 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
Init(0, Rles), Init(1, Rgre);
vector<pair<pair<int, int>, Frac>> vec;
Frac mx = Frac(0, 1);
for (int i = 1; i <= n; ++i) {
for (int j = i, k = i; j < Rles[i] || k < Rgre[i]; ) {
auto Add = [&](int p) -> void {
vec.push_back({{i, p}, Frac(1ll * (s[p] - s[i - 1]) * (s[p] - s[i - 1]), p - i + 1)});
mx = max(mx, vec.back().second);
};
if (j == k) {
Add(j), j = Rgre[j], k = Rles[k];
}
else if ((j < Rles[i]) && (k >= Rgre[i] || j < k)) {
Add(j), j = Rgre[j];
}
else {
Add(k), k = Rles[k];
}
}
}
if (n > int(1e5)) {
mx.Print();
return 0;
}
sort(vec.begin(), vec.end(), [&](pair<pair<int, int>, Frac> a, pair<pair<int, int>, Frac> b) -> bool {
return a.second < b.second;
});
fvec.resize(vec.size()), arr.resize(vec.size());
for (int i = 0; i < vec.size(); ++i) {
fvec[i] = vec[i].second;
arr[i] = Node({0, vec[i].first.first, vec[i].first.second, i});
ans[arr[i].l][arr[i].r] = arr[i].v;
}
for (int l = 2; l <= n; ++l) {
for (int i = 1, j = l; j <= n; ++i, ++j) {
ans[i][j] = max(ans[i][j], max(ans[i][j - 1], ans[i + 1][j]));
}
}
cin >> m;
for (int i = 1, l, r; i <= m; ++i) {
cin >> l >> r;
fvec[ans[l][r]].Print();
}
return 0;
}
QOJ 3047 Wind of Change
P6199 [EER1] 河童重工 弱化版。
考慮在 \(T_1\) 上點分治,假設當前層根結點為 \(r\),設 \(pre_i\) 表示結點 \(i\) 到結點 \(r\) 在 \(T_1\) 上的距離,則對於結點 \(i\),所求即為 \(pre_i + \min\limits_{j \neq i} pre_j + dist(T_2, i, j)\)。
對於當前點分治到的所有點在 \(T_2\) 上建一棵虛樹。在虛樹上考慮,相當於點有點權,邊有邊權,對於每個 \(i\) 找一個 \(j(j \neq i)\),最小化 \(\text{path}(i, j)\) 的邊權加 \(j\) 的點權。那麼這是入門樹形 \(\text{dp}\),\(f_{i, 0/1}\) 表示 \(i\) 子樹內最小/次小權值(要求一個兒子只能貢獻一個點),\(g_i\) 表示 \(i\) 子樹外最小權值,做 \(2\) 遍樹形 \(\text{dp}\) 即可。
時間複雜度 \(O(n \log^2 n)\)。
Code
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
using LL = long long;
using Pii = pair<int, int>;
const int N = 2.5e5 + 5, M = 18;
const LL Inf = 1e18;
int n, fa2[N][M], dep2[N], dfn2[N], now;
LL f[N][2], g[N];
LL pre1[N], pre2[N], ans[N];
bool dvis[N], inv[N];
vector<Pii> e1[N], e2[N];
vector<pair<int, LL>> vt[N];
void Dfs2 (int u) {
dfn2[u] = ++now;
dep2[u] = dep2[fa2[u][0]] + 1;
for (int k = 1; k < M; ++k) {
fa2[u][k] = fa2[fa2[u][k - 1]][k - 1];
}
for (auto i : e2[u]) {
int v = i.first, w = i.second;
if (v != fa2[u][0]) {
pre2[v] = pre2[u] + w;
fa2[v][0] = u, Dfs2(v);
}
}
}
int Lca2 (int u, int v) {
if (dep2[u] < dep2[v]) swap(u, v);
for (int k = M - 1; ~k; --k) {
if (dep2[u] - dep2[v] >= (1 << k)) {
u = fa2[u][k];
}
}
if (u == v) return u;
for (int k = M - 1; ~k; --k) {
if (fa2[u][k] != fa2[v][k]) {
u = fa2[u][k], v = fa2[v][k];
}
}
return fa2[u][0];
}
void Solve (int u) {
int cnt = 0, rt = 0;
auto Find_root = [&](auto &self, int u, int r) -> int {
int siz = 1, mx = 0;
for (auto i : e1[u]) {
int v = i.first, w = i.second;
if (!dvis[v] && v != r) {
int r = self(self, v, u);
mx = max(mx, r), siz += r;
}
}
if (mx <= cnt / 2 && cnt / 2 <= siz - 1) {
rt = u;
}
return siz;
};
cnt = Find_root(Find_root, u, 0);
Find_root(Find_root, u, 0);
vector<int> vec;
auto Dfs = [&](auto &self, int u, int r) -> void {
vec.push_back(u), inv[u] = 1;
for (auto i : e1[u]) {
int v = i.first, w = i.second;
if (!dvis[v] && v != r) {
pre1[v] = pre1[u] + w;
self(self, v, u);
}
}
};
pre1[rt] = 0, Dfs(Dfs, rt, 0);
auto cmp = [&](int i, int j) -> bool {
return dfn2[i] < dfn2[j];
};
sort(vec.begin(), vec.end(), cmp);
for (int i = 1, s = vec.size(); i < s; ++i) {
vec.push_back(Lca2(vec[i - 1], vec[i]));
}
sort(vec.begin(), vec.end(), cmp);
vec.resize(unique(vec.begin(), vec.end()) - vec.begin());
for (int i = 1; i < vec.size(); ++i){
int z = Lca2(vec[i - 1], vec[i]), x = vec[i];
vt[z].push_back({x, pre2[x] - pre2[z]});
vt[x].push_back({z, pre2[x] - pre2[z]});
}
auto DP1 = [&](auto &self, int u, int r) -> void {
f[u][0] = f[u][1] = Inf;
if (inv[u]) f[u][0] = pre1[u];
for (auto i : vt[u]) {
int v = i.first;
LL w = i.second;
if (v != r) {
self(self, v, u);
if (f[v][0] + w < f[u][0]) {
f[u][1] = f[u][0], f[u][0] = f[v][0] + w;
}
else {
f[u][1] = min(f[u][1], f[v][0] + w);
}
}
}
};
auto DP2 = [&](auto &self, int u, int r) -> void {
if (inv[u]) ans[u] = min(ans[u], pre1[u] + min(f[u][0] == pre1[u] ? f[u][1] : f[u][0], g[u]));
for (auto i : vt[u]) {
int v = i.first;
LL w = i.second;
if (v != r) {
g[v] = w + min(g[u], (f[v][0] + w == f[u][0] ? f[u][1] : f[u][0]));
self(self, v, u);
}
}
};
DP1(DP1, rt, 0), g[rt] = Inf, DP2(DP2, rt, 0);
for (int i = 0; i < vec.size(); ++i) {
vt[vec[i]].clear(), inv[vec[i]] = 0, pre1[vec[i]] = 0;
}
dvis[rt] = 1;
for (auto i : e1[rt]) {
if (!dvis[i.first]) Solve(i.first);
}
}
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, u, v, w; i < n; ++i) {
cin >> u >> v >> w;
e1[u].push_back({v, w});
e1[v].push_back({u, w});
}
for (int i = 1, u, v, w; i < n; ++i) {
cin >> u >> v >> w;
e2[u].push_back({v, w});
e2[v].push_back({u, w});
}
fill(ans + 1, ans + n + 1, Inf);
Dfs2(1), Solve(1);
for (int i = 1; i <= n; ++i) {
cout << ans[i] << '\n';
}
return 0;
}
QOJ5071 Check Pattern is Good
\(\text{dp}\) 不好做,貪心不好做,所以考慮網路流。
這裡將 W
和 B
換成 \(0/1\)。一個合法的 Pattern 相當於一個 \(01\) 交錯的 \(2 \times 2\) 矩形,它對答案有 \(1\) 的貢獻。
建立 \(0/1\) 變數模型,設 \(a_{i, j, k = 0/1} = 0/1\) 表示以 \((i, j)\) 為左上角的 \(2 \times 2\) 矩形,是否形成一個左上角為數字 \(k\) 的 Pattern,若 \(a_{i, j, k} = 1\) 則對答案有 \(1\) 的貢獻,由於是最小割,最大貢獻不好做,轉化成 \(a_{i, j, k} = 0\) 有 \(1\) 的代價,我們要最小化代價。
然後考慮有哪些限制。首先 \(a_{i, j, 0}\) 和 \(a_{i, j, 1}\) 不能同時取 \(1\),並且位置 \((i, j)\) 的值不能和 \((i, j + 1), (i + 1, j - 1), (i + 1, j), (i + 1, j + 1)\) 的值產生矛盾,最後就是不能和題目中給出的資訊衝突,如果這些限制全部不滿足,代價為 \(\infty\),顯然存在滿足所有限制的方案。
我們發現出現了對於 \(a = 0 \and b = 0\) 和 \(a = 1 \and b = 1\) 的限制,於是將所有變數 \(a_{i, j, k}\) 異或 \(k\),再異或 \((i + j) \bmod 2\),前者是 \(01\) 建模常用技巧了,後者是由於這道題的 Pattern 本身就形如 \(01\) 交錯,這容易使我們想到黑白染色。這樣處理後就是標準的 \(01\) 變數模型了(具體限制內容從略)。
由於是流題,時間複雜度不做分析。
Code
#include <iostream>
#include <vector>
#include <queue>
#include <cassert>
using namespace std;
const int Inf = 1e9;
namespace Dinic {
const int N = 2e4 + 5;
struct E {
int v, w, id;
};
int n, s, t;
int dis[N];
vector<E> e[N];
vector<E>::iterator now[N];
void Init (int _n, int _s, int _t) {
n = _n, s = _s, t = _t;
fill(e + 1, e + n + 1, vector<E>());
}
bool Bfs () {
fill(dis, dis + n + 1, -1), dis[s] = 0;
queue<int> q;
for (q.push(s); !q.empty(); q.pop()) {
int u = q.front();
for (auto i : e[u]) {
int v = i.v, w = i.w;
if (w && dis[v] == -1) {
dis[v] = dis[u] + 1;
q.push(v);
}
}
}
return dis[t] != -1;
}
int Dfs (int u, int d) {
if (u == t) return d;
int res = 0;
for (auto i = now[u]; i != e[u].end() && d; ++i) {
now[u] = i;
int v = i->v, &w = i->w, id = i->id;
if (dis[v] == dis[u] + 1 && w) {
int r = Dfs(v, min(w, d));
if (!r) dis[v] = -1;
res += r;
d -= r;
w -= r;
e[v][id].w += r;
}
}
return res;
}
int Max_flow () {
int res = 0;
while (Bfs()) {
for (int i = 1; i <= n; ++i) {
now[i] = e[i].begin();
}
res += Dfs(s, Inf);
}
return res;
}
void Add_edge (int u, int v, int w) {
e[u].push_back((E){v, w, int(e[v].size())});
e[v].push_back((E){u, 0, int(e[u].size()) - 1});
}
}
namespace _01_Limitation {
void Init (int n) {
Dinic::Init(n + 2, n + 1, n + 2);
}
void Lim1 (int x, int o, int w) {
if (!o) {
Dinic::Add_edge(x, Dinic::t, w);
}
else {
Dinic::Add_edge(Dinic::s, x, w);
}
}
void Lim2 (int x, int a, int y, int b, int w) {
assert(a ^ b == 1);
if (a && !b) swap(x, y);
Dinic::Add_edge(x, y, w);
}
pair<int, vector<bool>> Solve () {
int res = Dinic::Max_flow();
vector<bool> vec(Dinic::n - 2);
for (int i = 0; i < vec.size(); ++i) {
vec[i] = Dinic::dis[i + 1] == -1;
}
return {res, vec};
}
}
const int N = 105;
int n, m;
int a[N][N];
int Id (int x, int y, int o) { return (x - 1) * (m - 1) + y + o * (n - 1) * (m - 1); }
int main () {
cin.tie(0)->sync_with_stdio(0);
int T;
cin >> T;
while (T--) {
cin >> n >> m;
_01_Limitation::Init((n - 1) * (m - 1) * 2);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
char c;
cin >> c;
if (c == 'W')
a[i][j] = 0;
else if (c == 'B')
a[i][j] = 1;
else
a[i][j] = -1;
}
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
int v = (i ^ j) & 1;
_01_Limitation::Lim2(Id(i, j, 0), v ^ 1, Id(i, j, 1), v, Inf);
_01_Limitation::Lim1(Id(i, j, 0), v, 1);
_01_Limitation::Lim1(Id(i, j, 1), v ^ 1, 1);
if (j + 1 < m) {
_01_Limitation::Lim2(Id(i, j, 0), v ^ 1, Id(i, j + 1, 0), v, Inf);
_01_Limitation::Lim2(Id(i, j, 1), v, Id(i, j + 1, 1), v ^ 1, Inf);
}
if (i + 1 < n) {
_01_Limitation::Lim2(Id(i, j, 0), v ^ 1, Id(i + 1, j, 0), v, Inf);
_01_Limitation::Lim2(Id(i, j, 1), v, Id(i + 1, j, 1), v ^ 1, Inf);
}
if (i + 1 < n && j + 1 < m) {
_01_Limitation::Lim2(Id(i, j, 0), v ^ 1, Id(i + 1, j + 1, 1), v, Inf);
_01_Limitation::Lim2(Id(i, j, 1), v, Id(i + 1, j + 1, 0), v ^ 1, Inf);
}
if (i + 1 < n && j > 1) {
_01_Limitation::Lim2(Id(i, j, 0), v ^ 1, Id(i + 1, j - 1, 1), v, Inf);
_01_Limitation::Lim2(Id(i, j, 1), v, Id(i + 1, j - 1, 0), v ^ 1, Inf);
}
if (a[i][j] == 1 || a[i][j + 1] == 0 || a[i + 1][j] == 0 || a[i + 1][j + 1] == 1)
_01_Limitation::Lim1(Id(i, j, 0), v ^ 1, Inf);
if (a[i][j] == 0 || a[i][j + 1] == 1 || a[i + 1][j] == 1 || a[i + 1][j + 1] == 0)
_01_Limitation::Lim1(Id(i, j, 1), v, Inf);
}
}
pair<int, vector<bool>> ans = _01_Limitation::Solve();
for (int o = 0; o < 2; ++o) {
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
if (ans.second[Id(i, j, o) - 1] ^ o ^ ((i ^ j) & 1)) {
a[i][j] = o, a[i + 1][j] = o ^ 1, a[i][j + 1] = o ^ 1, a[i + 1][j + 1] = o;
}
}
}
}
cout << (n - 1) * (m - 1) * 2 - ans.first << '\n';
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cout << (a[i][j] == 1 ? 'B' : 'W');
}
cout << '\n';
}
}
return 0;
}
QOJ 5070 Check Pattern is Bad
猜結論:類似 NOI2024 D1T3 的,若當前局面無緊限制則一定有解(本題中,我們稱一個緊限制為一個 \(2 \times 2\) 矩形,滿足正好差一個 ?
形成 Pattern)。
考慮以下命題:
- 對於任意一個沒有緊限制的局面,我們任選一個當前為
?
的格子,它一定滿足:存在一種對於該格子的填寫方案(填W
或B
),使得緊限制只會增加不超過一個。
如果這個結論成立的話,那麼在無緊限制的情況下,我們這樣做只會使緊限制增加一個,並且一個緊限制的消除最多隻會產生一個緊限制,那麼我們依次處理就好了。
但是我們發現前面的結論是不對的,考慮以下情況:
WBW
???
BWB
此時對於中間的那個 ?
,無論填什麼都會使緊限制增加 \(2\) 個。
我們觀察什麼時候結論不成立,發現當且僅當,選出的格子作為中心的 \(3 \times 3\) 矩形中(若選出的格子在邊界上顯然成立),恰好有一行或一列是 ?
,且剩下的兩行/兩列 W
和 B
交錯,且這兩行/兩列恰好對偶。這個可以透過打表發現,也容易分討證明。
也就是說,所有使得上述結論不成立的 \(3 \times 3\) 矩形,都可以由前面給出的矩形旋轉得到(這也說明不成立的情況只有 \(4\) 種)。
但是我們容易發現,此時除了中心點,還有其他兩個 ?
可供選擇,所以我們只要每次選擇第一個?
就可以避免這種情況了。
時間複雜度 \(O(nm)\)。
Code
#include <iostream>
#include <queue>
#include <cassert>
using namespace std;
const int N = 105;
int n, m;
char a[N][N];
queue<pair<pair<int, int>, char>> q;
int Count (int x, int y) {
if (!x || !y || x >= n || y >= m) return 0;
return ((a[x][y] == 'B') + (a[x][y + 1] == 'W') + (a[x + 1][y] == 'W') + (a[x + 1][y + 1] == 'B')) % 3 == 0 &&
(a[x][y] == '?') + (a[x][y + 1] == '?') + (a[x + 1][y] == '?') + (a[x + 1][y + 1] == '?') == 1;
}
void Add_limit (int x, int y) {
char ch = ((a[x][y] == 'B') + (a[x][y + 1] == 'B') + (a[x + 1][y] == 'B') + (a[x + 1][y + 1] == 'B') == 2 ? 'B' : 'W');
for (int ax = x; ax <= x + 1; ++ax) {
for (int ay = y; ay <= y + 1; ++ay) {
if (a[ax][ay] == '?') {
q.push({{ax, ay}, ch});
}
}
}
}
int main () {
cin.tie(0)->sync_with_stdio(0);
int T;
cin >> T;
while (T--) {
[&]() -> void {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> a[i][j];
}
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
if (Count(i, j)) {
Add_limit(i, j);
}
}
}
for (int t = 1; t <= n * m; ) {
if (q.empty()) {
while (t <= n * m) {
int x = (t - 1) / m + 1, y = (t - 1) % m + 1;
if (a[x][y] == '?') {
char ret = '?';
for (int o = 0; o < 2; ++o) {
char ch = !o ? 'W' : 'B';
a[x][y] = ch;
if (Count(x - 1, y - 1) + Count(x - 1, y) + Count(x, y - 1) + Count(x, y) <= 1) {
ret = ch;
}
}
assert(ret != '?');
q.push({{x, y}, ret});
++t;
break;
}
++t;
}
if (q.empty()) break;
}
pair<pair<int, int>, char> tp = q.front();
q.pop();
int x = tp.first.first, y = tp.first.second;
char c = tp.second;
a[x][y] = c;
if (Count(x - 1, y - 1)) Add_limit(x - 1, y - 1);
if (Count(x - 1, y)) Add_limit(x - 1, y);
if (Count(x, y - 1)) Add_limit(x, y - 1);
if (Count(x, y)) Add_limit(x, y);
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
if (a[i][j] == 'B' && a[i][j + 1] == 'W' && a[i + 1][j] == 'W' && a[i + 1][j + 1] == 'B' ||
a[i][j] == 'W' && a[i][j + 1] == 'B' && a[i + 1][j] == 'B' && a[i + 1][j + 1] == 'W') {
cout << "NO" << '\n';
return;
}
}
}
cout << "YES" << '\n';
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cout << a[i][j];
}
cout << '\n';
}
}();
}
return 0;
}
QOJ 6406 Stage Clear
看到 \(n + m \le 72\) 這樣奇怪的限制,並且容易發現這題很難 Meet in the middle,所以考慮資料分治。
Solution1
考慮倒著做狀壓 \(\text{dp}\),設 \(f_S\) 表示當前局面下還剩點集 \(S\) 有怪沒打,此時需要 \(HP\) 為多少才能打贏這些怪,轉移列舉前一個打死的怪 \(x\),\(dp_{S \cup x} \gets \max(f_S - b_x, 0) + a_x\)。
時間複雜度 \(O(2^n \times n)\)。
Solution2
考慮另一種思路,注意到對於每一條邊 \(u \rightarrow v\),一定有 \(u < v\),這說明結點順序是原圖的一個拓撲序。所以最終方案一定可以表示為:從小往大加點,每次向一個已經存在的點連邊,構成一棵樹,限制就是要打一個點上的怪物,必須先打它的所有祖先。
對這棵樹的形態進行搜尋,一個入度為 \(d\) 的點有 \(d - 1\) 種連邊方案,由於題目相當於保證存在以 \(1\) 為根的 \(\text{dfs}\) 生成樹,去掉這些樹邊,考慮一條非樹邊只會讓一個點入度 \(+1\),且非樹邊只有 \(m - n + 1\) 條,所以樹的形態最多隻有 \(2^{m - n + 1}\) 種。
考慮圖是一棵樹怎麼做,這是一個經典的樹上貪心問題。
-
首先將 \(b_u \ge a_u\) 的點稱為優點,\(b_u < a_u\) 的點稱為劣點。
-
對於沒有樹上的祖先限制的情況(也有是給你一個集合,你可以以任意順序選),肯定是先選優點,再選劣點。優點裡面一定先選 \(a_u\) 更小的點,劣點裡面一定先選 \(b_u\) 更大的點。
-
現在擴充到樹上,我們將上面問題中第一個選的點稱為最優點 \(u\),我們發現樹上問題一定存在最優策略是,選完 \(u\) 的父親後馬上選 \(u\)。
-
每次找出當前最優點 \(u\)(\(u\) 不為根),將 \(u\) 和 \(u\) 的父親 \(r\) 合併。新點的資訊:\(a = \max(a_r, a_r - b_r + a_u), b = \max(b_r - a_u, 0) + b_u\)。把新點也加入最優點的考慮範圍。此時樹的點數減少了 \(1\),我們將問題規模從 \(n\) 縮小到了 \(n - 1\)。
-
重複以上過程 \(n - 1\) 次,直到樹上只有一個結點。
用堆維護最優點的選擇,時間複雜度 \(O(2^{m - n} n \log n)\),我比較懶,寫的暴力維護,時間複雜度 \(O(2^{m - n} n ^2)\)。
在 \(n \le 25\) 時跑 Solution1,在 \(n > 25\) 時跑 Solution2 即可。
Code
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
using namespace std;
using LL = long long;
const int N = 38;
const LL Inf = 1e18;
int n, m;
LL a[N], b[N];
namespace SolveA {
const int S = (1 << 25);
LL f[S];
int re[N];
int main () {
for (int u, v; m--; ) {
cin >> u >> v;
re[v] |= (1 << (u - 1));
}
fill(f + 1, f + (1 << n), Inf);
for (int s = 0; s < (1 << n); ++s) {
for (int x = 2; x <= n; ++x) {
if ((s >> (x - 1)) & 1) continue;
if ((s & re[x]) != re[x]) {
f[s | (1 << (x - 1))] = min(f[s | (1 << (x - 1))], max(f[s] - b[x], 0ll) + a[x]);
}
}
}
cout << f[(1 << n) - 2] << '\n';
return 0;
}
}
namespace SolveB {
vector<int> re[N];
int fa[N], rt[N];
LL a[N], b[N];
LL ans = Inf;
int Find (int x) {
if (rt[x] == x) return x;
return rt[x] = Find(rt[x]);
}
void Calc () {
copy(::a + 1, ::a + n + 1, SolveB::a + 1);
copy(::b + 1, ::b + n + 1, SolveB::b + 1);
auto cmp = [&](int i, int j) -> bool {
if (b[i] >= a[i]) {
if (b[j] >= a[j]) {
return a[i] < a[j];
}
return 1;
}
else if (b[j] >= a[j]) {
return 0;
}
return b[i] > b[j];
};
iota(rt + 1, rt + n + 1, 1);
for (int k = 1; k < n; ++k) {
int i = 0;
for (int j = 2; j <= n; ++j) {
if (rt[j] == j && (!i || cmp(j, i))) {
i = j;
}
}
int r = Find(fa[i]);
a[r] = max(a[r], a[r] - b[r] + a[i]), b[r] = max(0ll, b[r] - a[i]) + b[i];
rt[i] = Find(r);
}
ans = min(ans, a[1]);
}
void Dfs (int u) {
if (u == n + 1) {
Calc();
return;
}
for (auto v : re[u]) {
fa[u] = v;
Dfs(u + 1);
}
}
int main () {
for (int u, v; m--; ) {
cin >> u >> v;
re[v].push_back(u);
}
Dfs(2);
cout << ans << '\n';
return 0;
}
}
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 2; i <= n; ++i) {
cin >> a[i] >> b[i];
}
if (n <= 25) return SolveA::main();
return SolveB::main();
}