好题选讲2

你看这不就黄绿绿蓝。

A CF295B Greg and Graph

题意:给定一个有向图,每次删掉一个点,问每次操作后图剩下的所有点两两之间最短路之和。

看到删点就想到反过来加点。可以类似 Floyd 把新加的点与其他原有的点最短路算出来,单次添加是 \(O(n^2)\) 的,总共 \(O(n^3)\)

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 500 + 1, kI = 1e9;

int n, d[kN][kN], del[kN];
LL ans[kN];
bool t[kN];
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
      cin >> d[i][j];
    }
  }
  for (int i = 1; i <= n; i++) {
    cin >> del[i];
  }
  for (int x = n, k; x >= 1; x--) {
    k = del[x], t[k] = 1;
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= n; j++) {
        d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
      }
    }
    for (int i = 1; i <= n; i++) {
      for (int j = 1; j <= n; j++) {
        t[i] && t[j] && (ans[x] += d[i][j]);
      }
    }
  }
  for (int i = 1; i <= n; i++) {
    cout << ans[i] << ' ';  
  }
  return 0;
}

B CF1209G1 Into Blocks (easy version)

题意:给定一个长度为 \(n\) 的序列 \(a\),每次操作可以指定 \(i\)\(x\),表示将 \(a\) 中所有等于 \(a_i\) 的数全部改为 \(x\),代价为等于 \(a_i\) 的数的个数。求让序列 \(a\) 中所有相同元素在同一连通块中的最小代价。

因为要让所有相同元素在同一连通块,所以考虑从左往右扫,确定需要推平为同一元素的区间。
发现每当遇到一个元素,就找到他在序列中最后出现的位置,区间右端点直接跳到那里,直到扫描点追上右端点结束跳跃。这个区间的推平代价就是区间长度减去区间内众数出现次数。
几个区间代价加起来就行。时间复杂度 \(O(n)\)

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1;

int n, q;
int a[kN], r[kN], s[kN];

int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n >> q;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    r[a[i]] = i, s[a[i]]++;
  }
  int ans = 0;
  for (int i = 1; i <= n;) {
    int R = i, mx = 0;
    for (; i <= R; i++) {
      mx = max(mx, s[a[i]]);
      R = max(R, r[a[i]]);
    }
    ans += mx;
  }
  cout << n - ans << '\n';
  return 0;
}

C SP116 INTERVAL - Intervals

题意:有 \(n\) 个值域区间,在 \([a_i, b_i]\) 中至少选 \(c_i\) 个不同的整数,求至少取几个。

\(s_i\) 表示在 \([1, i]\) 中要选多少个不同整数。则区间要求转化为 \(s_{b_i} - s_{a_i - 1} \geq c_i\)
不难想到差分约束。除此之外,还有定义的一个隐藏条件:\(0 \leq s_i - s_{i - 1} \leq 1\)
这个图十分漂亮,也非常稀疏,可以放心大胆使用 SPFA。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <queue>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e4 + 1, kI = 1e9;

int T, n;
int s, t, d[kN];
vector<PII> e[kN];
bool f[kN];
queue<int> q;
int main() {
  cin.tie(0)->sync_with_stdio(0);
  for (cin >> T; T--;) {
    cin >> n;
    s = kI, t = -1;
    for (int i = 1, l, r, c; i <= n; i++) {
      cin >> l >> r >> c;
      e[l].emplace_back(r + 1, c);
      s = min(s, l), t = max(t, r + 1);
    }
    for (int i = s; i < t; i++) {
      e[i].emplace_back(i + 1, 0);
      e[i + 1].emplace_back(i, -1);
    }

    fill(d + s, d + t + 1, -kI);
    auto F = [](int x, int w) {
      if (d[x] < w) {
        d[x] = w;
        f[x] || (q.push(x), f[x] = 1);
      }
    };
    for (F(s, 0); !q.empty(); q.pop()) {
      int x = q.front();
      f[x] = 0;
      for (auto [v, w] : e[x]) {
        F(v, d[x] + w);
      }
    }
    cout << d[t] << '\n';

    for (int i = s; i <= t; i++) {
      e[i].clear();
    }
  }
  return 0;
}

D AGC010C Cleaning

题意:给定一颗树,每个节点有个 \(a_i\) 颗石子,每次操作可以选择两个叶子将他们之间最短路径上的所有点拿掉一个石子,问是否可能把所有石子拿完。

叶子之间的路径本质上是两个叶子向上的路径的拼合。
因此想到设 \(f_i\) 表示 \(i\) 子树内向上贡献的路径数量。尝试做树形 dp 去做判断。
发现 \(a_i\) 可以用 \(f_i\) 来表示。在 \(i\) 这个子树中,有两种对儿子贡献上来路径的解决方案。

  1. 继续向上贡献。依据定义这个数量是 \(f_i\)
  2. 将两条路径拼一起。设儿子贡献上来的边数和 \(\sum\limits_{v\in son_i} f_v\)\(g_i\), 没向上贡献的边有 \(g_i - s_i\) 条,两两配对则方案数为 \(\frac{g_i - f_i}{2}\) 条。

每个这样的方案都会取走 \(i\) 上的一颗石子。所以 \(a_i = f_i + \frac{g_i - f_i}{2} = \frac{g_i + f_i}{2}\)
\(f_i = 2a_i - g_i\)。可以由叶子向根递归依次算出。
至于怎么判断结果是否可行,只需要判断 \(f_i\) 是否合法。

  1. \(f_{root} = 0\)。根不能有多的路径向上贡献,否则取不完石子。
  2. \(0\leq f_i\leq a_i\)。贡献上去的路径数必须是非负整数,且不超过 \(a_i\),否则第 \(i\) 个点石子不够取。
  3. \(\max\limits_{v\in son_i} \leq \frac{g_i - f_i}{2} + f_i = a_i\)。因为要和别的儿子拼合,所以每个儿子用于拼合的路径数不超过总数的一半。这个儿子最多能霸占所有的向上贡献的路径,所以加上 \(f_i\)

根直接指定一个非叶子当就行了。根是谁对答案影响不大。特判一下 \(n=2\) 只有叶子的情况。

时间复杂度 \(O(n)\),正确性显然,每一步都是强制性要求,总是不劣的。

Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;

int n, a[kN];
vector<int> e[kN];
int f[kN];

void D(int x, int fa) {
  f[x] = (e[x].size() == 1 ? a[x] : 2 * a[x]);
  for (auto v : e[x]) {
    if (v == fa) {
      continue;
    }
    D(v, x);
    f[x] -= f[v];
    if (f[v] > a[x]) {
      cout << "NO\n";
      exit(0);
    }
  }
  if (f[x] > a[x] || f[x] < 0) {
    cout << "NO\n";
    exit(0);
  }
}
int main() {
  cin.tie(0)->sync_with_stdio(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  for (int i = 1, u, v; i < n; i++) {
    cin >> u >> v;
    e[u].push_back(v), e[v].push_back(u);
  }
  if (n == 2) {
    cout << (a[1] == a[2] ? "YES\n" : "NO\n");
    return 0;
  }
  int rt = -1;
  for (int i = 1; i <= n; i++) {
    if (e[i].size() != 1) {
      rt = i;
      break;
    }
  }
  D(rt, 0);
  cout << (f[rt] == 0 ? "YES\n" : "NO\n");
  return 0;
}
posted @ 2024-04-11 16:27  Lightwhite  阅读(34)  评论(0)    收藏  举报