LG P1875 佳佳的魔法药水 题解
思路
我们可以从 DP 的角度来考虑这道题。
设
通常情况下,在“图状”的 DP 时,遇到环形依赖,有两个解决办法:
- 利用 Dijkstra 的贪心思想,即状态之间转移时,代价都是正的,所以能把目前队列中的最小代价的状态确定为一个最优状态。换句话说,Dijkstra 是在“有单调性的图上”,利用单调性可以确定出一个拓扑序,按照这个序列进行转移就能得到正确答案。
- 借助 SPFA 算法进行动态规划,即在这张图上多次迭代,最后把答案收敛出来。
本题两种方法都能做,但大部分题解都用的是 Dijkstra,所以这里我们采用 SPFA 来进行迭代求解。
先解决最小值怎么求:
我们将药水抽象为点,将“
每个点都有个基础状态,即花费为
对于每个点,枚举能到达它的边(以组为单位),计算当前点
这样就能得到
可能有人会有疑问,为什么不在计算最短路的同时记录最短路的条数呢?因为那种记录最短路的条数的思想也是基于 DP 的思想,只有在拓扑序下转移才能被正确的求出来,然而 SPFA 不是按照拓扑序进行转移的。
我们可以采用记忆化搜索的方式进行计算,因为整个最短路径的 DAG 图已经被构造出来了,递归回去计算就行。
代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1010, M = 1000001;
int n, cnt = 0;
int f[N], head[N], w[N], tot[N];
bool vis[N];
struct Edge {
int a, b, next;
};
Edge e[M];
vector<int> belongs[N];
void add(int a, int b, int v) {
e[++cnt] = {a, b, head[v]};
head[v] = cnt;
}
void spfa() {
queue<int> q;
// 将每个点入队
for (int i = 0; i <= n - 1; i++) {
q.push(i);
f[i] = w[i];
vis[i] = 1;
}
while (!q.empty()) {
int u = q.front();
q.pop();
vis[u] = 0;
for (int i = head[u]; i != -1; i = e[i].next) {
// 枚举每组边
int a = e[i].a, b = e[i].b;
if (f[u] > f[a] + f[b]) {
f[u] = f[a] + f[b];
// 如果药水u的花费能够降低,那么由药水u合成出来的药水的花费都有可能降低
// 所以要把这些点都放回队列里
for (auto v: belongs[u])
if (vis[v] == 0)
vis[v] = 1, q.push(v);
}
}
}
}
int dfs(int u) {
if (tot[u] != 0) return tot[u];
if (f[u] == w[u]) tot[u]++;
for (int i = head[u]; i != -1; i = e[i].next) {
int a = e[i].a, b = e[i].b;
if (f[u] == f[a] + f[b]) {
tot[a] = dfs(a);
tot[b] = dfs(b);
tot[u] += tot[a] * tot[b];
}
}
return tot[u];
}
int main() {
ios::sync_with_stdio(0);
cout.tie(0);
cin.tie(0);
cin >> n;
for (int i = 0; i < n; i++) cin >> w[i], head[i] = -1;
int a, b, c;
while (cin >> a >> b >> c) {
add(a, b, c);
if (a == b) continue;
// 这里记录a和b分别能合成哪些药水,等会儿状态更新时会用到
belongs[a].push_back(c);
belongs[b].push_back(c);
}
spfa();
dfs(0);
cout << f[0] << " " << tot[0] << endl;
return 0;
}
总结
- 以 DP 的思想来思考图论问题其实很常见,比如分层图问题从 DP 角度看就是,是二维 DP 问题(一维为点,二维为附加状态,比如用了几次免费),如果这道题你会了,那么这道题你也一定会做啦,快去试试吧!