2022 China Collegiate Programming Contest (CCPC) Mianyang Onsite E Hammer to Fall
知识点:DP,根号分治
Link:https://codeforces.com/gym/104065/problem/E
一个转移受时间影响的、对操作序列按时间分块的、根号分治的经典套路。
vp 的时候 dztlb 大神把关键的 DP 转化开出来了但是吃了四十发过不了发现实现假了呃呃。之前 vp 22 西安的时候也遇到了思想完全一致的根号分治题,然而那题直接大力上 LCT 草过去了,导致这题做不出来,输!
简述
给定一 \(n\) 个节点 \(m\) 条边的无向连通图 \(G\),边有边权 \(w\)。初始时第 \(i\) 个节点上有 \(a_i\) 个人。你可以在任意时间进行任意次操作,每次操作将某个节点上的一个人立刻转移到相邻的节点上,代价为经过的边的权值。
现在按时间顺序有 \(q\) 次事件发送,第 \(i\) 次事件将给定节点编号 \(b_i\),表示在时刻 \(i\) 节点 \(b_i\) 上将降下重锤干掉该节点所有人。
现在要通过若干操作保证没有人被干掉,求所有操作代价之和的最小值,答案对 998244353 取模。
\(2\le n\le 10^5\),\(1\le m,q\le 10^5\),\(1\le a_i, w\le 10^9\),\(1\le b_i\le n\)。
3S,1024MB。
分析
显然仅会在每次重锤即将降下之前,将目标节点上的人全部转移,且一定是将这些人全部一起转移到相邻的某一个节点上。于是对于每个时刻的最优决策,仅需考虑之后所有时刻的情况即可,于是套路地考虑倒序枚举时间进行转移。
记 \(f_{i, u}\) 表示倒序枚举到时刻 \(i\),在节点 \(u\) 上的一个人不被干掉的最小代价。则显然有:
答案即:
显然上述方程中第一维是完全不必要的,可以直接优化掉。由于某些点度数是过多,直接实现上述 DP 时间复杂度为 \(O(n^2)\) 级别,显然不可接受,但是发现有 \(m\le 10^5\),于是套路地考虑按照度数根号分治。
考虑设定阈值 \(B\),对于度数不大于 \(B\) 的节点直接按照上述转移方程做;对于度数大于 \(B\) 的点,考虑预处理其邻接点的信息便于直接转移。但根据上述转移过程,预处理的邻接点中可能有某些点在预处理之后又被更新到了,从而导致直接转移出错。
于是一个套路是再对时间进行分块。考虑对度数大于 \(B\) 的每个节点预处理其邻接点 \(v\) 中 \(f_{v}\) 的前 \(B+1\) 小,且每经过时间 \(B\) 就 \(O(m)\) 地大力重构所有节点预处理的信息,转移时仅需枚举预处理的上述 \(B+1\) 个邻接点进行转移即可。因为每个时间块内至多进行 \(B\) 次操作,显然可以保证一定枚举到了最优的转移。
重构时考虑枚举所有点的邻接点,并使用 nth_element
取前 \(B+1\) 小,总时间复杂度为 \(O\left(m\frac{q}{B}\right)\) 级别,单次转移时间复杂度为 \(O(B)\) 级别,总时间复杂度 \(O\left(n + m + qB + m\frac{q}{B}\right)\) 级别,取 \(B = \sqrt m\) 时上式取最小值为 \(O\left(n + m + q\sqrt m\right)\)。
代码
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
const LL kInf = 9e18 + 2077;
const LL p = 998244353;
//=============================================================
int n, m, q, b[kN];
LL a[kN];
struct Edge {
int v;
LL w;
};
std::vector<Edge> edge[kN];
struct Node {
LL f, w;
int v;
bool operator < (const Node sec_) const {
if (f != sec_.f) return f < sec_.f;
return w < sec_.w;
}
};
LL f[kN];
std::vector<Node> s[kN];
int nowtime, lim;
//=============================================================
void rebuild() {
for (int i = 1; i <= n; ++ i) {
if ((int) edge[i].size() <= lim) continue;
s[i].clear();
for (auto [v, w]: edge[i]) s[i].push_back((Node) {f[v] + w, w, v});
std::nth_element(s[i].begin(), s[i].begin() + lim + 1, s[i].end());
}
}
void update(int u_) {
f[u_] = kInf;
if ((int) edge[u_].size() <= lim) {
for (auto [v, w]: edge[u_]) f[u_] = std::min(f[u_], f[v] + w);
return ;
}
for (int i = 0; i <= lim; ++ i) {
auto [ff, w, v] = s[u_][i];
f[u_] = std::min(f[u_], f[v] + w);
}
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m >> q;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
for (int i = 1; i <= m; ++ i) {
int u, v, w; std::cin >> u >> v >> w;
edge[u].push_back((Edge) {v, w});
edge[v].push_back((Edge) {u, w});
}
for (int i = 1; i <= q; ++ i) std::cin >> b[i];
lim = lim = sqrt(2 * m) + 1;
rebuild();
for (int i = q; i; -- i) {
if (++ nowtime == lim) rebuild(), nowtime = 0;
update(b[i]);
}
LL ans = 0;
for (int i = 1; i <= n; ++ i) (ans += f[i] * a[i] % p) %= p;
std::cout << (LL) (ans % p) << "\n";
return 0;
}