EDU-CFR-143解题报告

A. Two Towers

题意:有两个积木塔,由红色/蓝色积木组成,你每次可以将一个塔的塔顶放到另一个塔上,问任意次操作后能否使得两座塔都是红蓝相间。

可以将一个塔全部转移到另一个塔上,那么操作相当于把“大塔”从中间分开两半。如果“大塔”中红红/蓝蓝的数量超过 1,则不可行,否则从那里分开即可。

By tourist

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  int tt;
  cin >> tt;
  while (tt--) {
    int n, m;
    cin >> n >> m;
    string a;
    string b;
    cin >> a >> b;
    reverse(b.begin(), b.end());
    a += b;
    int cnt = 0;
    for (int i = 0; i < n + m - 1; i++) {
      cnt += (a[i] == a[i + 1]);
    }
    cout << (cnt <= 1 ? "YES" : "NO") << '\n';
  }
  return 0;
}

B. Ideal Point

题意:数轴上有若干条线段,问能否删去若干条,使 x 成为被覆盖最多的点(严格最多)。

容易发现,只要有两个线段,一个左端点在 x,一个右端点在 x,那么只要保留这两条就行。如果不存在这种情况,一定不可行,因为要想 x 覆盖比 x+1 更多,就必然要存在一个线段右端点为 x;要想 x 覆盖比 x1 更多,就必然要存在一个线段左端点为 x

By tourist

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  int tt;
  cin >> tt;
  while (tt--) {
    int n, k;
    cin >> n >> k;
    vector<int> l(n), r(n);
    for (int i = 0; i < n; i++) {
      cin >> l[i] >> r[i];
    }
    bool L = false;
    bool R = false;
    for (int i = 0; i < n; i++) {
      L |= (l[i] == k);
      R |= (r[i] == k);
    }
    cout << (L && R ? "YES" : "NO") << '\n';
  }
  return 0;
}

C. Tea Tasting

题意:有 n 杯茶和 n 个人,第 i 杯茶有 ai 单位的茶水,第 i 个人对每杯茶最多和 bi 单位。初始第 i 个人站在第 i 杯茶前,每人喝一次,然后 n 个人往左平移一位(第一个人结束),继续重复上面步骤。问每个人最后分别喝到了多少茶。

我们可以从左到右扫每杯茶,看这杯茶对后面的人造成的贡献。首先可以预处理前缀和,二分查找这杯茶到第几个人被喝完,这样,这个人之前的人每人都能“喝饱”;最后一个人喝掉剩下的茶。区间修改可以在差分数组上改两位,最后求前缀和即可。

By jiangly

void solve() {
    int n;
    std::cin >> n;
    std::vector<int> a(n), b(n);
    for (int i = 0; i < n; i++)
        std::cin >> a[i];
    for (int i = 0; i < n; i++)
        std::cin >> b[i];
    std::vector<i64> s(n + 1);
    for (int i = 0; i < n; i++)
        s[i + 1] = s[i] + b[i];
    std::vector<int> d(n);
    std::vector<i64> ans(n);
    for (int i = 0; i < n; i++) {
        int j = std::lower_bound(s.begin() + i, s.end(), a[i] + s[i]) - s.begin() - 1;
        if (j > i) {
            d[i]++;
            if (j < n)
                d[j]--;
        }
        if (j < n)
            ans[j] += a[i] - (s[j] - s[i]);
    }
    for (int i = 1; i < n; i++)
        d[i] += d[i - 1];
    for (int i = 0; i < n; i++) {
        ans[i] += 1LL * d[i] * b[i];
        std::cout << ans[i] << " \n"[i == n - 1];
    }
}

D. Triangle Coloring

题意:有 6n 个点,其中 {1,2,3},{4,5,6},,{6n2,6n1,6n} 分别形成三角形。边有边权。你需要将 3n 个点染成黑色,3n 个点染成白色,定义一种方案的权值为,所有两端异色的边的权值和。求让权值和最大化的方案数,对 998244353 取模。

题目的大体理解是要尽可能多的异色边,所以可以先考虑最多情况。容易发现,一个三角形内最多只能有两个异色边,且让每个三角形都有两个是可以做到的(在 2n 个三角形中,n 个黑黑白,n 个白白黑)。于是,最优解一定是每个三角形选较大的那两条边。

对于三角形内的方案数,如果较小的两条边不同,那么只有一种方案;如果三条边相同,则有三种方案;如果较小的两条边相同,较大的不同,则有两种。

由于要在 2n 个三角形中选出 n 个染黑黑白,剩余白白黑,所以需要再乘一个 (2nn) 的组合数。

By tourist

int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  int n;
  cin >> n;
  Mint ans = 1;
  for (int i = 0; i < n; i += 3) {
    vector<int> a(3);
    for (int j = 0; j < 3; j++) {
      cin >> a[j];
    }
    sort(a.begin(), a.end());
    if (a[0] < a[1]) {
      ans *= 1;
    }
    if (a[0] == a[1] && a[1] < a[2]) {
      ans *= 2;
    }
    if (a[0] == a[1] && a[1] == a[2]) {
      ans *= 3;
    }
  }
  for (int i = n / 6 + 1; i <= n / 3; i++) {
    ans *= i;
  }
  for (int i = 1; i <= n / 6; i++) {
    ans /= i;
  }
  cout << ans << '\n';
  return 0;
}

E. Explosions?

题意:有 n 个怪物,第 i 个有 ai 滴血,每次普通攻击花费 1 能量,可以让一个怪物减 1 滴血。在若干次普通攻击后,可以发一个大招,消耗 x 能量,选择某个怪物(设为 i)减 x 滴血,如果杀死,则相邻怪物减 ai1 滴血;如果继续杀死相邻怪物,则有传递性(如杀死 i+1i+2 怪物会减 ai+11 滴血)。你必须通过这一个大招杀掉所有剩下的怪物。问总的最小能量消耗。

显然要让普通攻击尽可能少,且把阵型优化成能够连续杀完的形态。可以考虑 DP:记 f[0][i] 表示如果大招对 i 用,要想把左边都连续干掉,需要提前处理的普通攻击次数;f[1][i] 为右边,同理。

以左边为例,f[0][1] 显然为 0,直接干掉就行了;每次考虑下一个新的怪物时,如果上一个怪物血量更高,则需要减少到 ai1(为了保证连续),而这可能会导致上上一个血量需要继续减少,似乎很难考虑。

但是,我们发现,对于一个连续段,要减少只能统一减少,所以可以维护当前的连续段信息。具体地,维护一个栈,元素形如 {l,r,lx,rx} 表示这一个连续段的左右端点和左右端点的值。

当加入一个怪物时,可以新建一个连续段 {i,i,ai,ai},然后从栈顶开始依次处理段之间的影响。容易发现,影响时自顶向下的,且一个段影响下一个段后,这两个段必然合并。这便是复杂度保证的关键。

细节上,需要注意血量到 0 就不需要继续处理了,而前面处理的过程中可能会把血量减到负数,从而多花一些操作。只需要处理结束后“恢复”这些操作即可(操作数减一个等差数列)。

By cxm1024

int a[300010], f[2][300010];
void Solve(int test) {
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	struct node {int l, r, lx, rx;};
	for (int tt = 0; tt <= 1; tt++) {
		stack<node> st;
		int now = 0;
		for (int i = 1; i <= n; i++) {
			st.push({i, i, a[i], a[i]});
			while (st.size() > 1) {
				node tmp = st.top(); st.pop();
				if (tmp.lx <= st.top().rx) {
					now += (st.top().rx - tmp.lx + 1) * (st.top().r - st.top().l + 1);
					st.top().lx -= st.top().rx - tmp.lx + 1, st.top().r = tmp.r, st.top().rx = tmp.rx;
				}
				else {st.push(tmp);break;}
			}
			if (st.top().lx < 0) {
				now -= (-st.top().lx) * (-st.top().lx + 1) / 2;
				st.top().l += (-st.top().lx), st.top().lx = 0;
			}
			f[tt][i] = now;
		}
		reverse(a + 1, a + n + 1);
	}
	reverse(f[1] + 1, f[1] + n + 1);
	int ans = 1e18;
	for (int i = 1; i <= n; i++)
		ans = min(ans, a[i] + f[0][i] + f[1][i]);
	cout << ans << endl;
}

F. Blocking Chips

题意:有一颗树,有若干个点为黑色且放了棋子,其他点为白色。你可以依次操作每个棋子,将它移动一步到白色点,并将该点染成黑色。操作完最后一个棋子后返回第一个棋子循环操作。问最多操作次数。

容易发现,棋子走后就无法回头,所以相当于对于每个棋子,选出一条以该棋子为端点的路径。

这种最大化的问题,看上去就非常像二分答案。考虑二分操作次数,那么前面若干个棋子会走 x+1 步,后面会走 x 步。预先存储下来,判断可行性。

略微思考一下性质,容易发现一个棋子要尽可能往子树内走,这样能给上面的棋子留下更多空间。于是可以考虑 DP:f[i] 表示 i 的子树能往下走的最远深度(如果需要往上走,就是负数)。

那么,对于一个有棋子的点,假设需要走 x 步,考虑所有儿子。只要有一个儿子需要往上走,就不合法;否则如果有一个儿子能供这个点往下走 x 步,f[i]=0(这个子树不需要往上走,也不允许往下走);否则就必须往上走 x 步,f[i]=x

对于一个没有棋子的点,考虑所有儿子。如果有两个儿子需要往上走,就不合法;如果有一个儿子需要往上走,条件允许的话可以“拐个弯”再往下走(即通过别的子树的空地,内部解决问题,不需要往上走)。否则就必须要往上走。

看起来很多情况,但其实很多都可以统一写法:使用 neg 表示当前考虑这些儿子,需要走的量(不可能有两个往上走,一旦出现立刻无解)。对于棋子来说,初始为 x;对于空地,初始为 1。如果一个儿子的 DP 值为负数(需要往上走),且当前的 neg0(表示已经需要占用这个节点)则无解。否则,将 neg 设为该 DP 值 1(因为已经往这个节点走了一格)。同时,记录儿子允许空地的最大值 max,遍历完后,如果 negmax 则可以内部解决,f[i]=0,否则必须往上走,f[i]=neg

By jiangly

void solve() {
    int n;
    std::cin >> n;

    std::vector<std::vector<int>> adj(n);
    for (int i = 1; i < n; i++) {
        int u, v;
        std::cin >> u >> v;
        u--, v--;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    int k;
    std::cin >> k;
    std::vector<int> a(k);
    for (int i = 0; i < k; i++) {
        std::cin >> a[i];
        a[i]--;
    }

    int lo = 0, hi = n;
    while (lo < hi) {
        int x = (lo + hi + 1) / 2;

        std::vector<int> f(n, -1);
        for (int i = 0; i < k; i++) {
            f[a[i]] = x / k + (i < x % k);
        }

        std::vector<int> dp(n);

        bool ok = true;
        auto dfs = [&](auto self, int x, int p) -> void {
            int neg = -f[x], max = 0;
            for (auto y : adj[x]) {
                if (y == p) {
                    continue;
                }
                self(self, y, x);
                if (dp[y] < 0) {
                    if (neg <= 0) {
                        ok = false;
                    }
                    neg = dp[y] + 1;
                } else {
                    max = std::max(max, dp[y]);
                }
            }

            if (neg <= 0) {
                if (neg + max >= 0) {
                    dp[x] = 0;
                } else {
                    dp[x] = neg;
                }
            } else {
                dp[x] = max + 1;
            }
            // std::cerr << "dp[" << x << "] = " << dp[x] << "\n";
        };
        dfs(dfs, 0, -1);

        // std::cerr << "x = " << x << "\n";
        if (ok && dp[0] >= 0) {
            lo = x;
        } else {
            hi = x - 1;
        }
    }
    std::cout << lo << "\n";
}

这种统一写法的方式非常值得学习,我赛时的代码就是直接分类讨论情况,又难写又不好调试。写代码之前要三思而后行。

posted @   曹轩鸣  阅读(22)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起