简明题意
思路
从深搜开始
手玩样例,我们很快(从题目后的提示)找到了 1 − 3 1 - 3 1−3, 1 − 2 − 3 1-2-3 1−2−3, 1 − 2 − 3 1 - 2 - 3 1−2−3(两者不同)一共三条路径。求出其权值之和再加上乘船返回两次所需的时间,和样例恰好一致。
于是我们便可以把这个问题分成两个子问题逐个求解:
- 起点到终点的所有不同路径的权值总和
- 起点到终点的所有不同路径的数目
选择从起点开始 dfs
,我们需要考虑在搜索树上,我们怎样进行状态与状态之间的转移(说白了就是 dfs
函数里写什么)。
我们对症下药:题目让求起点到终点路径的权值和,我们就先存下起点到当前点路径的权值和,用 s[]
数组存储;题目让求起点到终点路径的数目,我们就先存下起点到当前点路径的数目,用 cnt[]
数组存储。
搜索树上,父亲想要告诉儿子什么呢?肯定就是 s[fa]
和 cnt[fa]
的数值了。那么作为父亲最喜爱的儿子(误),怎样处理好父亲给的信息呢?我们具体分析。
假设我们当前节点(父亲)为 2
,即将处理的边为 权值为 $ 7 $ 的边,那么儿子节点就是 3
。 我们发现有了这条边之后,1 -> 2
的每一条路径都可以和它组合,成为 1 -> 3
的一条新路径。
因此这条边对于权值之和的贡献是:通过 1 -> 2
的每一条不相同的路和这条边的权值总和。这条边对于路径数量的贡献,就是 1 -> 2
的路径数量。
比如:假设 1
到 2
有
3
3
3 条边,2
到 3
有
5
5
5 条边,那么根据乘法原理 1 -> 2 -> 3
有
3
×
5
3 \times 5
3×5 条边,在我们分别处理 2
到 3
的
5
5
5 条边时,每一条边都会造成
3
3
3 条新路径的贡献,这和乘法原理是相符的。对于权值的求和也可以从上面的假设去理解。
因此我们处理一条从 u
指向 to
,边权为 w
的边,那么:
s t o = ∑ w × c n t u + s u s_{to}= \sum w \times cnt_u + s_u sto=∑w×cntu+su
而对于 to
,路径数量则是:
c n t t o = ∑ c n t u cnt_{to} = \sum cnt_u cntto=∑cntu
有如下代码:
void dfs(int u)
{
for (int k = head[u]; k; k = nxt[k])
{
cnt[to] += cnt[u];
s[to] += mul(edge[k], cnt[u])) + s[u];
dfs(to);
}
}
然而当你用这个做法兴奋地提交之后,才发现自己获得了 20
分(或者是 0
分)的好成绩。
调整搜索顺序
你可能会抱怨道:为什么!我样例明明过了啊!然而作为一名 OIer, 绝对不能 Too Young Too Simple
,样例往往太简单,只是辅助理解题意的,并不能帮你找出 bug 来。
我再给一组样例,在你读完题解前,希望对你有所启发:
输入为:
4 6 1 3 1
1 2 1
1 2 1
2 3 1
1 4 1
1 4 1
4 3 1
输出为:
11
如图,由于边输入顺序的原因(我选用链式前向星存图),导致 4
号点在还没有处理完,得到正确的 s
与 cnt
值时,就向 3
传递了信息,最后造成统计错误。
因此我们希望,改变搜索顺序,让每一个节点把自己的数据处理好,再向下一个节点汇报。 有没有这样的方法呢?答案是有的,那就是拓扑排序。
我们记录每一个节点此时的入度,处理完一条边入度自减 $ 1 $,当入度为 $ 0 $ 时加入搜索队列,那么我们就可以保证每个点都得到了正确的结果。
代码
因此最终代码如下:(注意处理取模)
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
const int MAXN = 1e4 + 50, MAXM = 5e4 + 50, INF = 0x3f3f3f3f, MOD = 1e4;
int head[MAXN], nxt[MAXM], ver[MAXM], edge[MAXM], tot; // 链式前向星相关
int cnt[MAXN], s[MAXN], in[MAXN]; // 本题相关
bool vis[MAXM]; // 记录某条边是否走过(或许不需要)
inline void add(int u, int v, int w)
{
in[v]++; // 记录入度
ver[++tot] = v;
edge[tot] = w;
nxt[tot] = head[u];
head[u] = tot;
}
inline int mul(int a, int b) { return ((a % MOD) * (b % MOD)) % MOD; } // 一步三取模,被取模错误丢分吓怕了
inline void topo(int start) // 拓扑排序的同时进行递推
{
queue<int> q;
cnt[start] = 1;
q.push(start);
while (q.size())
{
int u = q.front(); q.pop();
for (int k = head[u]; k; k = nxt[k])
{
if (vis[k]) continue;
int to = ver[k];
vis[k] = true; in[to]--;
cnt[to] = (cnt[to] + cnt[u]) % MOD;
s[to] = (((s[to] % MOD) + mul(edge[k], cnt[u])) % MOD + s[u]) % MOD;
if (in[to] == 0) q.push(to); // 入度为 0 加入队列
}
}
}
int main()
{
int n, m, start, t, ts;
scanf("%d %d %d %d %d", &n, &m, &start, &t, &ts); // 简简单单的输入
for (int i = 1; i <= m; ++i)
{
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
}
topo(start);
int ans = (s[t] + mul(ts, cnt[t] - 1) + MOD) % MOD; // 别忘了把路径数量 - 1乘上乘船时间
printf("%d", ans);
return 0;
}