Educational Codeforces Round 145 (E. Two Tanks 详细讲解 + 代码注释)

时间:2021-05-15 01:17:18

E. Two Tanks

链接:E. Two Tanks
推荐大佬的视频讲解:B站 yingluosanqian
大晚上琢磨jiang老师的代码看不明白,还好找到这个视频讲解,以下题解基于该视频讲解,仅稍作个人理解,建议大家先看视频有配图更直观。

题意

给定两个水桶 A , B A, B A,B 容量分别为 a , b a,b a,b,以及 n n n 个操作 v i v_{i} vi。当 v i > 0 v_{i} > 0 vi>0 表示从 A A A 中倒 v i v_{i} vi 升水到 B B B, v i < 0 v_{i} < 0 vi<0 则代表从 B B B 中倒水进 A A A倒水过程中水不会超过瓶子容量,若满则停,也不会倒出超过瓶子中已有的水量,至多倒空。
求当两个瓶子中初始水量分别为 c , d c, d c,d A A A 中的最终水量,其中 0 < = c < = a 0<=c<=a 0<=c<=a 0 < = d < = b 0<=d<=b 0<=d<=b。共有 ( a + 1 ) ∗ ( b + 1 ) (a + 1) * (b + 1) (a+1)(b+1) 种可能,用矩阵表示。

题解

先将倒水的问题抽象出来:
互相倒水的操作可以看做是一个 [ − c , d ] [-c, d] [c,d] 的线段在区间 [ − a , b ] [-a, b] [a,b] 中做平移运动,运动时必须与 0 0 0 有交点,也不能超出区间 [ − a , b ] [-a, b] [a,b]
v i > 0 v_{i} > 0 vi>0 是将线段整体向右平移, v i < 0 v_{i} < 0 vi<0 是将线段整体向左移,而倒水的那些限制也能体现,只能在区间内运动是水量不能超出瓶子的容量,必须与 0 0 0 有交点是水量为 x x x 时不能倒出超过 x x x,至多倒空。

这里放一张上文所说的视频up主的讲解片段,图中的即为所描述的抽象结果,绿线为 0 0 0 端点。
Educational Codeforces Round 145 (E. Two Tanks 详细讲解 + 代码注释)

首先按 c + d c + d c+d 的总量来分类讨论,对于 第一个瓶子的初始水量设为 c c c,根据上述线段运动的描述可以想象到,存在一个区间 [ l , r ] [l, r] [l,r], 当 c c c 处在这个区间内时可以完整执行所有的 n n n 个操作而不会触发这些限制。 可能存在 l > r l > r l>r 的情况,说明不存在这样区间。
于是我们的情况有:

  1. l < = c < = r l <= c <= r l<=c<=r: 直接记录 n n n 个操作的前缀和 a n s = c − p r e ans = c - pre ans=cpre,若不存在 [ l , r ] [l, r] [l,r] 这样的区间就一定是下面两种情况之一。
  2. c < = l c <= l c<=l:肯定存在某次操作会使得向右的操作受到限制 (线段左端点已经运动到 0 0 0 / 线段右端点已经运动到 b b b)。
    那么这次操作过后的线段在区间的位置会和 c = l c = l c=l 的情况相同,于是最终答案输出 c = l c = l c=l 的即可。
  3. c > = r c >= r c>=r:同理一样会存在某个操作使得向左的操作收到限制,最终答案与 c = r c = r c=r 相同。

关于如何求区间 [ l , r ] [l, r] [l,r], 可以求出最终可能的最小水量和最大水量,我们设极限情况下线段经过 n n n 个操作后恰好达到上下界。
于是我们按这两个极限情况逆推 n n n 个操作回去,就能得到操作不受限制的上下界。

具体实现看代码以及注释

代码

时间有点晚,照着jiang老师的代码打了一遍,但加上了自己的理解注释,基本和jiang老师的相同,或许可以先看看大佬的代码:jiangly的代码

自己的代码:

#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr); cout.tie(nullptr);
    
    int n, a, b;
    cin >> n >> a >> b;

    vector<int> v(n);
    for (int i = 0; i < n; i ++) {
        cin >> v[i];
    }

    // 设 A 为第一个水瓶,B 为第二个水瓶
    vector< vector<int> > ans(a + 1, vector<int>(b + 1));
    for (int s = 0; s <= a + b; s ++) { // 枚举初始A,B水量之和
        /* n 次操作后 A 中水量的上下界 */
        int x = max(0, s - b); // A 可能的最小水量
        int y = min(a, s); // 可能的最大水量

        /* 用上下界模拟逆过程找到 [l, r] */
        int lo = x, hi = y; 
        for (int i = n - 1; i >= 0; i --) {
            lo += v[i];
            hi += v[i];
            lo = max(lo, x); // 逆过程时进行修正,保证不会超过上下界
            hi = min(hi, y);
        }
        // 逆过程结束后就得到最终的题解中所说的 [l, r]
        
        /* 再用所得[l, r]进行一遍正向操作 */
        int flo = lo, fhi = hi; // 可以得到初始分别为两个边界的最终结果
        for (int i = 0; i < n; i ++) {
            flo = max(min(flo - v[i], y), x);
            fhi = max(min(fhi - v[i], y), x);
        }

        for (int c = x; c <= y; c ++) {
            int d = s - c;
            if(c <= lo) { // 小于等于 l 的
                ans[c][d] = flo;
            }
            else if (c >= hi) { // 大于等于 r 的
                ans[c][d] = fhi;
            }
            else { // 存在区间[l, r]
                ans[c][d] = flo + (c - lo); // 区间最小值l的答案 + 超过l的部分
            }
        }
    }   

    for(int c = 0; c <= a; c ++) {
        for(int d = 0; d <= b; d ++) {
            cout << ans[c][d] << " ";
        }
        cout << "\n";
    }
    
    return 0;
}