题解 P1073 【最优贸易】

fy1234567ok

2017-11-01 16:59:40

Solution

# 分层图状态转移 + SPFA 其实此题可以**不用强连通分量缩点**,还有更优美的解法,**只需40行代码**。 主要思想是类似“分层图”,或者类似“DAG”(有向无环图)的状态转移思想,特别是针对这种状态量相互影响的问题,分层图思想很实用。 ## 分析 读完这道题,可以发现这样的事实: - 你可以在图上任意走动 - 最终答案只与你的买入与卖出价格有关(我们就把买入卖出价值作为边权) - 如果你买入了一个水晶球,你是没有不卖它的道理的(显然咯,买了不卖血亏...) 暴力算法不难得出: 我只关心我在哪里买了这个水晶球,在哪里把它卖出去,并且,我能否从起点走到我的买入点,从买入点走到卖出点,然后在走到 $n$ 因此,先枚举两个点再bfs检查能否到达,然后更新答案。 而此题的难点在与你如何知道你是否能够到达买入,卖出,终点(即上两行 并且 后面我说的话),和你能否把所有可能的情况考虑在内。 分层图可以很好的解决这个问题。 由于可以任意走动,所以我们可以建一张图,令图上的边全都是 $0$ ,表示我的走动对我最终的结果没有影响。 考虑某个点 $i$ ,它买入或者卖出水晶球的花费是 $v_i$ 。 那么: **当我们进行买入操作**,我们就建立一条**有向边**转移到一张新图上,边的大小为 $-v_i$ ,它从第一层的点 $i_{0}$ ~~指向点 $i$ 所能到达的点(在第二层图上)~~ _指向第二层的点 $i_{1}$_ 。而这张新图就是我们的第二层图。 它表示:假如我选择走了这条边,就是我在这个点买了这个水晶球,我不会反悔,并且我接下来考虑在某个点卖它。 **当我们进行卖出操作**,我们建立一条有向边转移到第三层图上,边的大小为 $v_i$,它从第二层的点 $i_{1}$ ~~指向点 $i$ 所能到达的点(在第二层图上)~~ _指向第三层的点 $i_{2}$_ 。 它表示:假如我选择走了这条边,就是我在这个点卖了这个水晶球,我不会反悔,并且我接下来考虑走向终点。 > 注:不能指向 $i$ 下一层到达的点,因为这样意思就变成了:我在这个点买入了水晶球,*并且我一定从这个点走出去*。多了一层意思就不一样了 可以发现,从第一层图走到第二层图走到第三层图走到终点,这就是一个合法的决策。 对于**任何**一种决策都可以抽象为我从 $1$点 走到点 $x$ 买入,然后走到点 $y$ 卖出, 然后走到点 $n$。而每一种决策都在图中对应了一条从 $1_0$ 到 $x_0$ 到 $x_1$ 到 $y_1$ 到 $y_2$ 到 $n_2$ 的路径。 ![](https://cdn.luogu.com.cn/upload/image_hosting/0hulu3d3.png) 所以分层图把所有合法的决策都考虑到了。而我们要求的最大收益就正好对应了图上的从 $1_0$ 到 $n_2$ 最长的路径。 > 注:当然也可以把边权都改成负的求个最短路(因为不存在负环也是可以的) ### 最后解释一下为什么我们要分层: 因为当你分了层,你就可以从还未买入这个状态,转移到已经买入准备卖出这个状态,然后在转移到从卖出点走向终点的状态。由于有向边的建立,你不能从第二/三层走回第一层图,这保证了你只做一次买卖,而不是无限做买卖,符合了题目的要求 而我们最终的答案,就是求从第一层图的 $1$ 号点,走道第三层图的 $n$ 号点的最长路,如图所示。 ![](https://cdn.luogu.com.cn/upload/image_hosting/fxq0pi14.png) 到此,这道题就解完了。 ### UPDATE 2020.10.6 其实2年前我已经退役,抱歉鸽了这么久才更新。感谢在评论中帮我指出错误,提出建议的大佬们。这次更新修改了建模,符号加入了LaTeX, 优化了代码。Hack数据已经可以通过了。如果发现了其他问题,欢迎在评论区中讨论,感谢各位的支持。 # 代码如下 ``` cpp #include<bits/stdc++.h> using namespace std; const int maxn = 1e5 + 5; int n, m, d[maxn*3], inq[maxn*3]; vector<pair<int, int>> G[maxn*3]; #define t(x,i) (x+i*n) // t(x,i) 表示第i层的x // 建立x->y边的函数, 不用加 make_pair是 C++11特性 #define add(x, y) G[t(x,0)].push_back({t(y,0), 0}), G[t(x,1)].push_back({t(y,1),0}), G[t(x,2)].push_back({t(y,2),0}) void spfa(int s) { for(int i = 1;i <= n*3;i++) d[i] = INT_MIN; // 这里n*3别漏了, INT_MIN 是C++内置最小值常量 d[s] = 0; queue<int> Q; inq[s] = true; Q.push(s); while(!Q.empty()) { int x = Q.front(); Q.pop(); inq[x] = false; for(auto [v, len] : G[x]) // C++17 特性, 等价于 int v = G[x][i].first, len = G[x][i].second; if(d[v] < d[x] + len) { d[v] = d[x] + len; if(!inq[v]) { Q.push(v); inq[v] = true; } } } } int main() { ios_base::sync_with_stdio(0); cin.tie(0); // 加速cin, cout cin >> n >> m; for(int i = 1, v;i <= n; ++i) { cin >> v; G[t(i,0)].push_back({t(i,1), -v}); G[t(i,1)].push_back({t(i,2), v}); } for(int i = 1,x,y,z;i <= m; ++i) { cin >> x >> y >> z; add(x, y); if(z == 2) add(y, x); } spfa(t(1,0)); cout << d[t(n,2)] << endl; return 0; } ```