The 2025 ICPC Asia Chengdu Regional Contest (The 4rd Universal Cup. Stage 4: Grand Prix of Chengdu) 题解
- Problem A. A Lot of Paintings
- Problem B. Blood Memories
- Problem C. Crossing River
- Problem D. Deductive Snooker Scoring
- Problem E. Escaping from Trap
- Problem F. Following Arrows
- Problem G. GCD of Subsets
- Problem H. Heuristic Knapsack
- Problem I. Inside Polygon
- Problem J. Judging Papers
- Problem K-Coverage
- Problem L. Label Matching
- Problem M. Meeting for Meals
神秘场,或者说神仙打架场
榜单强的恐怖,金牌线好像是八道题(至少 codeforces 上好像是这么写的)
其实大部分题都是可做题,但是有两个题对灵感需求很高
题目 E, H, F 比其他题目难度上明显断档了,导致这场十题队非常多(
质量很好,貌似有一个好玩的思想贯彻了很多题:正确的贪心可以减少很多分类讨论。包括在 C、D、H、和 M 都有这个思想的体现
F 那个构造题确实不好写,以后有时间再做吧。(都欠了多少题了)
Problem A. A Lot of Paintings
每一个数 \(a_i\) 都会对应一个原始数字 \(p \leq 1\),这个原始数字有一个可取的下界和 可能 不可取的上界。
- 当 \(a_i \neq 100\) 的时候 \(p\) 的上界就是 \(p + 0.05\)(不可取)
- 当 \(a_i \neq 0\) 的时候 \(p\) 的下界就是 \(p - 0.05\)(可取)
明白了这两点后就可以得到原始数字求和的可能范围,只要 \(1\) 在这个范围之内那就是合法的,然后就可以拿去构造答案了。由于上界是可能可取可能不可取的,这里要简单判一下(说白了就是 \(a_i\) 求和是不是 \(100\))
构造答案就相当于当你对 \(a_i\) 进行修改之后四舍五入还是 \(a_i\),但是求和要等于 \(100\)。
现在分类讨论,记 \(s=\sum a_i\)
- \(s = 100\):真的需要考虑吗?
- \(s < 100\):将差的那一点点均匀涂抹到所有的 \(a_i\) 上面,具体说就是 \(a_i\) 加上 \(\frac{100 - s}{n}\),考虑精度问题可以将所有数字乘上 \(n\)。容易知道这种情况下一定不会触发进位,因为我们做判定的时候就已经保证这一点。
- \(s > 100\):直接将所有 \(a_i\) 乘 \(10\),这样每个 \(a_i\) 最多可以减去 \(5\)。然后尽可能减减到 \(\sum a_i=1000\)。
#include <iostream>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t, n;
int b[kMaxN];
void solve() {
int sum = 0, cnt1 = 0, cnt2 = 0;
cin >> n;
for (int i = 1; i <= n; i++) cin >> b[i], sum += b[i], cnt1 += !!b[i], cnt2 += b[i] != 100;
if (2 * sum - cnt1 > 200) return cout << "No" << endl, void();
if (cnt2) {
if (200 >= cnt2 + 2 * sum) return cout << "No" << endl, void();
} else if (200 > 2 * sum)
return cout << "No" << endl, void();
cout << "Yes" << endl;
if (sum == 100) {
for (int i = 1; i <= n; i++) cout << b[i] << ' ';
} else if (sum > 100) {
sum *= 10;
sum -= 1000;
for (int i = 1; i <= n; i++) {
b[i] = b[i] * 10;
}
for (int i = 1; i <= n; i++) {
if (b[i]) {
if (sum <= 5)
b[i] -= sum, sum = 0;
else
sum -= 5, b[i] -= 5;
}
cout << b[i] << ' ';
}
} else {
for (int i = 1; i <= n; i++) cout << n * b[i] + (100 - sum) << ' ';
}
cout << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem B. Blood Memories
嗯写这个题的时候忘记广义矩阵乘法了……
比较简单的题吧,由于 \(n\) 很小所有可以暴力枚举操作状态,然后写出来转移矩阵,套一个矩阵快速幂就结束了。很水的题。
比较需要注意的是广义矩阵乘法下的矩阵单位元不好找,做快速幂的时候别去找单位元初始化了。
#include <iostream>
#include <cstring>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int T;
int n, m, k, R;
int popcnt[kMaxN];
struct matrix {
int a[64][64];
int* operator[](int k) { return a[k]; }
matrix operator*(matrix& b) {
matrix tmp;
for (int i = 0; i < 64; i++) {
for (int j = 0; j < 64; j++) {
tmp[i][j] = 0;
for (int k = 0; k < 64; k++) {
upmax(tmp[i][j], a[i][k] + b[k][j]);
}
}
}
return tmp;
}
};
int a[kMaxN], c[kMaxN];
int cost(int state) {
int sum = 0;
for (int i = 1; i <= n; i++) {
if ((state >> (i - 1)) & 1) sum += c[i];
}
return sum;
}
int dam(int state) {
int sum = 0;
for (int i = 1; i <= n; i++) {
if ((state >> (i - 1)) & 1) sum += a[i];
}
return sum;
}
void solve() {
// 每一轮的消耗仅取决于上一轮的消耗
// 考虑维护转移:头状态s,尾状态t,长度 2^k(至多30
// 2^12 * 30,空间复杂度完全够
cin >> n >> m >> k >> R;
matrix tmp, ans;
memset(tmp.a, 0, sizeof(tmp.a));
memset(ans.a, 0, sizeof(ans.a));
for (int i = 1; i <= n; i++) {
cin >> a[i] >> c[i];
}
int all = 1 << n;
for (int s = 0; s < all; s++) {
for (int t = 0; t < all; t++) {
tmp[s][t] = -1e9;
int c1 = cost(s), c2 = cost(t);
if (c2 + popcnt[s & t] * k <= m && c1 <= m) {
tmp[s][t] = dam(t);
// cerr << s << ' ' << s << ' ' << c1 << ' ' << c2 << endl;
// cerr << c2 + popcnt[s & t] * k << endl;
}
}
}
for (int i = 0; i < all; i++) {
ans[0][i] = (cost(i) <= m ? dam(i) : -1e9);
}
R--;
for (int i = 0; i < 32; i++) {
if ((R >> i) & 1) ans = ans * tmp;
tmp = tmp * tmp;
}
int max = 0;
for (int i = 0; i < all; i++) upmax(max, ans[0][i]);
cout << max << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
popcnt[0] = 0;
for (int i = 0; i <= 64; i++) {
popcnt[i << 1] = popcnt[i];
popcnt[i << 1 | 1] = popcnt[i] + 1;
}
cin >> T;
while (T--) solve();
return 0;
}
Problem C. Crossing River
神秘题
我好像之前在哪里见过这种题,可能是 USACO?比较 trick 的题,有点吃灵感说真的
答案合法性显然具有单调性,考虑二分答案然后判定
正着做判定其实不好做,因为很难得知需要是否需要等待某个人或者等待多久。考虑做最短路状态数也达到了 \(O(n^2)\) 这个级别。
然后就考虑反着做判定……反着判定就很简单了而且有很明显的贪心特征:每个人渡河的时间是固定的,要做的就是尽可能减少浪费的时间……
感觉就是要找一个正确抽象将题目变成具有贪心特征的题目……
#include <iostream>
#include <algorithm>
#include <random>
#include <tuple>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n, m, k;
int a[kMaxN], b[kMaxN], id1[kMaxN], id2[kMaxN];
std::vector<std::tuple<int, int, int>> tmp;
bool check(int t, int beg) {
tmp.clear();
int now = beg, i = n, j = m;
while (i + j) {
// cerr << t << ' ' << now << ' ' << i << ' ' << j << endl;
if (now == 0) {
if (j) {
if (t - k < b[id2[j]]) return false;
tmp.push_back({t - k, 1, id2[j]});
now ^= 1, j--, t -= k;
} else {
if (t < k) return false;
t -= k, now ^= 1;
}
} else {
if (i) {
if (t - k < a[id1[i]]) return false;
tmp.push_back({t - k, 0, id1[i]});
now ^= 1, i--, t -= k;
} else {
if (t < k) return false;
t -= k, now ^= 1;
}
}
}
return true;
}
void solve() {
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) cin >> a[i], id1[i] = i;
for (int i = 1; i <= m; i++) cin >> b[i], id2[i] = i;
std::sort(id1 + 1, id1 + 1 + n, [](int i, int j) { return a[i] < a[j]; });
std::sort(id2 + 1, id2 + 1 + m, [](int i, int j) { return b[i] < b[j]; });
int l = 0, r = 1e17;
int anst = -1;
std::vector<std::tuple<int, int, int>> ans;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid, 0) || check(mid, 1)) {
r = mid - 1, ans = tmp, anst = mid;
} else {
l = mid + 1;
}
}
cout << anst << endl;
std::reverse(ans.begin(), ans.end());
for (auto [t, p, id] : ans) {
cout << t << ' ' << p << ' ' << id << endl;
}
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
solve();
return 0;
}
Problem D. Deductive Snooker Scoring
我一开始打算写分类讨论的,这个题就简单分讨其实也不难。
观察到状态数非常非常少,我们直接从起点状态做一个 \(bfs\) 然后记录一下转移就……结束了……
#include <cassert>
#include <queue>
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int a, b, n, p;
std::string str;
struct type {
int n, a, b, p;
} lst[22][201][201][2];
bool vis[22][201][201][2];
std::string link[22][201][201][2];
void bfs() {
std::queue<type> q;
auto record = [&](int n, int a, int b, int p, int ln, int la, int lb, int lp, const std::string& s) {
if (vis[n][a][b][p]) return;
vis[n][a][b][p] = true;
lst[n][a][b][p] = {ln, la, lb, lp};
q.push({n, a, b, p});
link[n][a][b][p] = s;
};
record(21, 0, 0, 0, -1, -1, -1, -1, "");
while (!q.empty()) {
auto [n, a, b, p] = q.front();
q.pop();
if (n == 0) continue;
record(n, a, b, p ^ 1, n, a, b, p, "/");
// 还没有到清场阶段
if (n > 6) {
for (char s = '2'; s <= '7'; s++) {
if (p)
record(n - 1, a, b + s - '0' + 1, p, n, a, b, p, (std::string) "1" + s);
else
record(n - 1, a + s - '0' + 1, b, p, n, a, b, p, (std::string) "1" + s);
}
if (p)
record(n - 1, a, b + 1, p ^ 1, n, a, b, p, (std::string) "1" + "/");
else
record(n - 1, a + 1, b, p ^ 1, n, a, b, p, (std::string) "1" + "/");
} else {
// 清场阶段
int s = (6 - n) + 2;
std::string str = "0";
str.back() += s;
if (p)
record(n - 1, a, b + s, p, n, a, b, p, str);
else
record(n - 1, a + s, b, p, n, a, b, p, str);
}
}
}
std::string ans;
void make_ans(int n, int a, int b, int p) {
if (a == -1) return;
auto [ln, la, lb, lp] = lst[n][a][b][p];
make_ans(ln, la, lb, lp), ans += link[n][a][b][p];
}
void solve() {
cin >> a >> b >> n >> p;
cerr << vis[n][a][b][p] << endl;
if (!vis[n][a][b][p]) return cout << "NA" << endl, void();
ans.clear(), make_ans(n, a, b, p);
cout << ans << endl;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
bfs();
int t;
cin >> t;
while (t--) solve();
return 0;
}
Problem E. Escaping from Trap
逆天题
纯数学题
考虑将多边形边长强行锁死成 \(1\)。由于是正多边形,所以可以强行规定一下点的坐标。
考虑点 \(A, B, C\),假设查询了 \(S_{\Delta PAB}\),\(S_{\Delta PAC}\),那么根据这两个面积的比值可以确定 \(P\) 的轨迹在一个二次曲线上,由于做比值所以消除了多边形边长 \(d\) 的影响。
根据简单的中学知识可以知道 \(P\) 的轨迹会退化成两条直线,当然平方去掉符号影响后直接去算也是可以得知 \(P\) 的轨迹喽……
假定查询了 \(S_{\Delta PAD}\),那么就可以唯一确定一条经过 \(A\) 的直线为 \(P\) 的轨迹。也就是说通过三个三角形的面积就可以确定一条 \(P\) 的轨迹。这个时候再去找几个就可以确定另外一条轨迹直线了,求一下交点就可以了。
思路是这样的,但是代码细节好像有点多……我没有写挂了好多次……不想写了 QAQ。
Problem F. Following Arrows
很难的构造题。以后有时间再写吧。
Problem G. GCD of Subsets
签到题,写不出退役吧
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
long long n, k, m;
void solve() {
cin >> n >> k >> m;
long long p = n / k;
long long only = 1, two = (p - 1) / 2, extra = n - only - two * 2;
only += std::min(m, extra), m -= std::min(m, extra);
if (m >= two * 2) {
only += two * 2, m -= two * 2, two = 0;
} else {
long long max = m / 2;
two -= max, m -= max * 2, only += max * 2;
}
cout << only + two << endl;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem H. Heuristic Knapsack
神仙题,把我的智力按在地板上疯狂摩擦。
通过强大的贪心技巧减少了一万个分类讨论的典型
而且涉及到了非常可怕的 trick,建议看官方题解,写的很好(好吧事实上就是我也觉得这个题过于神秘不可做了)
#include <iostream>
#include <algorithm>
#include <random>
#include <vector>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n, W;
int w[kMaxN], r[kMaxN];
std::vector<int> A, B, sA;
int w2[kMaxN], r2[kMaxN];
std::vector<int> id1, id2;
bool check(int k) {
for (int i = 1; i <= n; i++) w2[i] = w[i], r2[i] = r[i] ? r[i] : 1;
int res = W;
for (int i = 0; i < k; i++) {
res -= w[A[i]];
if (r[A[i]] == 0) r2[A[i]] = 1e9;
}
if (res < 0) return false;
// 为 b 中的重量重新标定
int j = 0;
if (k < A.size())
while (j < B.size() && res) {
int limit = 1e9;
if (W == 1e9) limit--;
upmin(limit, res);
if (A[k] < B[j]) {
upmin(limit, w[A[k]] - 1);
} else {
upmin(limit, w[A[k]]);
}
if (limit == 0) {
w2[B[j++]] = 1e9;
} else {
w2[B[j++]] = limit, res -= limit;
}
}
// cerr << "END2" << endl;
// if (k == A.size() && W == 1e9 && j < B.size() && res) {
// w2[B[j++]] = 1;
// }
while (j < B.size()) w2[B[j++]] = 1e9;
std::vector<bool> vis1(n + 1, false), vis2(n + 1, false);
std::sort(id1.begin(), id1.end(), [](int i, int j) { return (w2[i] == w2[j] ? i < j : w2[i] < w2[j]); });
std::sort(id2.begin(), id2.end(), [](int i, int j) { return (r2[i] == r2[j] ? i < j : r2[i] > r2[j]); });
res = W;
for (auto& i : id1) {
if (w2[i] <= res) res -= w2[i], vis1[i] = true;
}
res = W;
for (auto& i : id2) {
if (w2[i] <= res) res -= w2[i], vis2[i] = true;
}
for (int i = 0; i <= n; i++) {
if (vis1[i] != vis2[i]) return false;
}
return true;
}
void solve() {
cin >> n >> W;
id1.resize(n), A.clear(), B.clear();
for (int i = 0; i < n; i++) id1[i] = i + 1;
id2 = id1;
for (int i = 1; i <= n; i++) cin >> w[i], (w[i] ? A : B).push_back(i);
for (int i = 1; i <= n; i++) cin >> r[i];
std::sort(A.begin(), A.end(), [](int i, int j) {
if (w[i] == w[j]) return i < j;
return w[i] < w[j];
});
std::sort(B.begin(), B.end(), [](int i, int j) {
if (r[i] == r[j]) return i < j;
return r[i] > r[j];
});
// 若一个方案是合法的,那么最后二者选取一定是 A 的一个前缀并且同时也是 B 的一个前缀
// 枚举 A 的一个前缀
for (int i = 0; i <= A.size(); i++) {
if (check(i)) {
cout << "Yes" << endl;
for (int i = 1; i <= n; i++) cout << w2[i] << ' ';
cout << endl;
for (int i = 1; i <= n; i++) cout << r2[i] << ' ';
cout << endl;
return;
}
}
cout << "No" << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem I. Inside Polygon
简单的计算几何。好写。
对于外凸包的任意一个点,可以 \(O(n)\) 初始化出内凸包的外切线并且 \(O(1)\) 维护。
容易证明,三角形的边一定要在外切线的两边。假设考虑外凸包的节点 \(i\),那么可以获得一个逆时针连续区间和一个顺时针连续区间,记作 \(R_i\) 和 \(L_i\),在这个区间里的点都可以和 \(i\) 构成一个三角形边。
\(R_i\) 和 \(L_i\) 的初始化整体上的复杂度是线性的
题目就转变成了:存在多少点对 \(i, j, k\),使得 \(j \in R_i, k \in R_j, i \in R_k\)。当然等价于\(j \in R_i, k \in R_j, k \in L_i\)
到这里其实比较简单了,遍历 \(i\),但后可以动态维护有多少个点对 \(j, k\) 使得 \(j \in R_i, k \in R_j\),然后求一个区间和来筛选 \(k \in L_i\) 的点数量。
这个就是一个区间和区间加的事情,由于性质比较好其实可以做到线性。我比较懒套了一个线段树(
#include <iostream>
#include <vector>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 4e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
struct node {
int x, y;
node operator+(const node& b) const { return {x + b.x, y + b.y}; }
int operator*(const node& b) const { return x * b.y - y * b.x; }
node operator-(const node& b) const { return {x - b.x, y - b.y}; }
friend node operator*(int i, const node& a) { return {i * a.x, i * a.y}; }
};
int t;
int n, m;
std::vector<node> p1, p2;
int R[kMaxN], L[kMaxN];
int lst(int x, int n) {
return (x == 0 ? n - 1 : x - 1);
}
int nxt(int x, int n) {
return (x == n - 1 ? 0 : x + 1);
}
bool leq(const node& a, const node& b) {
return a * b >= 0;
}
int a[kMaxN];
int laz[kMaxN];
void add(int p, int v, int l, int r) {
a[p] += (r - l + 1) * v;
laz[p] += v;
}
void pushdown(int p, int l, int r) {
if (laz[p]) {
add(p << 1, laz[p], l, (l + r) >> 1);
add(p << 1 | 1, laz[p], ((l + r) >> 1) + 1, r);
laz[p] = 0;
}
}
void pushup(int p) {
a[p] = a[p << 1] + a[p << 1 | 1];
}
void add(int p, int l, int r, int L, int R, int v) {
if (L > R) return add(p, l, r, 1, R, v), add(p, l, r, L, n, v);
if (L <= l && r <= R) return add(p, v, l, r);
pushdown(p, l, r);
int mid = (l + r) >> 1;
if (L <= mid) add(p << 1, l, mid, L, R, v);
if (mid + 1 <= R) add(p << 1 | 1, mid + 1, r, L, R, v);
pushup(p);
}
int ask(int p, int l, int r, int L, int R) {
// cerr << "YOU ASK " << p << ' ' << l << ' ' << r << ' ' << L << ' ' << R << endl;
if (L > R) return ask(p, l, r, 1, R) + ask(p, l, r, L, n);
if (L <= l && r <= R) return a[p];
pushdown(p, l, r);
int sum = 0, mid = (l + r) >> 1;
if (L <= mid) sum += ask(p << 1, l, mid, L, R);
if (mid + 1 <= R) sum += ask(p << 1 | 1, mid + 1, r, L, R);
return sum;
}
void solve() {
cin >> n;
p1.resize(n);
for (auto& [x, y] : p1) cin >> x >> y;
cin >> m;
p2.resize(m);
for (auto& [x, y] : p2) cin >> x >> y;
for (int i = 0; i < (n << 2); i++) a[i] = laz[i] = 0;
int l = 0, r = 0;
for (int i = 0; i < m; i++) {
auto t = p2[i] - p1[0];
if (leq(p2[l] - p1[0], t)) l = i;
if (leq(t, p2[r] - p1[0])) r = i;
}
L[0] = R[0] = 0;
while (leq(p2[l] - p1[0], p1[lst(L[0], n)] - p1[0])) L[0] = lst(L[0], n);
for (int i = 0; i < n; i++) {
if (i) L[i] = L[i - 1], R[i] = R[i - 1];
while (leq(p2[nxt(r, m)] - p1[i], p2[r] - p1[i])) r = nxt(r, m);
while (leq(p2[l] - p1[i], p2[nxt(l, m)] - p1[i])) l = nxt(l, m);
while (!leq(p2[l] - p1[i], p1[L[i]] - p1[i])) L[i] = nxt(L[i], n);
while (leq(p1[nxt(R[i], n)] - p1[i], p2[r] - p1[i])) R[i] = nxt(R[i], n);
}
int now = 0, ans = 0;
for (int i = 0; i < n; i++) {
while (lst(now, n) != R[i]) {
add(1, 1, n, now + 1, R[now] + 1, 1);
now = nxt(now, n);
}
add(1, 1, n, i + 1, R[i] + 1, -1);
ans += ask(1, 1, n, L[i] + 1, i + 1);
}
ans /= 3;
cout << ans << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
// cerr << "BEGIN" << endl;
cin >> t;
while (t--) solve();
return 0;
}
Problem J. Judging Papers
写不出退役
#include <iostream>
#include <algorithm>
#include <vector>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n, m, k, b;
void solve() {
cin >> n >> m >> k >> b;
int ans = 0;
std::vector<int> list;
for (int i = 1; i <= n; i++) {
int sum = 0, cnt = 0, change = 0;
for (int j = 1, v; j <= m; j++) {
cin >> v;
sum += v;
change += v + (v <= 0 ? 1 : -1);
}
if (sum >= k)
ans++;
else if (change >= k && b)
b--, ans++;
}
cout << ans << endl;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem K-Coverage
这个题有线性做法woc
写了一坨 \(O(n\log n)\) 的做法
将操作拆成两部分来考虑:
- 删除起点为 \(i\) 的一条线段
- 在起点 \(j\) 加入一条线段,下文将这个行为称作决策 \(j\)
下文用 \(cnt[k]\) 来表示某段区间中某个数的数量,显然在不做任何操作的情况下答案就是 \(cnt[k]\)
删除 \(i\) 的线段,对答案的贡献为:\(-cnt[k] + cnt[k + 1]\)
在 \(j\) 加入一条线段,对答案的贡献为:\(-cnt[k] + cnt[k - 1]\)
但是,如果 \(i\) 和 \(j\) 有重叠的话,重叠区域的贡献计算是要诡异一点的,要同时去掉删除和加入的两个影响,也就是说,重叠区域会有一坨额外的影响:\(2cnt[k] - cnt[k + 1] - cnt[k - 1]\)。
我们在做滑动窗口去枚举 \(i\) 的时候其实可以动态维护某个点究竟与多少个决策 \(j\) 有重叠、然后把这个偏移量加到决策 \(j\) 上,会影响的决策 \(j\) 显然是连续的,相当于用一个线段树了来动态维护就可以了。
事实上反思目前的操作,你在枚举 \(i\) 的时候,会影响两段 \(j\),一段是重叠区域越来越少的,另一段是重叠区域越来越多的,但是两段之内的 \(j\) 决策的相对优秀性是不会改变的,感觉套个单调队列可以优化到线性了。
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 2e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n, L, k;
int l[kMaxN];
int a[kMaxN];
int cntK[kMaxN];
int cntKd[kMaxN];
int cntKp[kMaxN];
int N;
int laz[kMaxN << 2];
int max[kMaxN << 2];
#define mid ((l + r) >> 1)
void add(int p, int l, int r, int v) {
max[p] += v, laz[p] += v;
}
void pushdown(int p, int l, int r) {
if (laz[p]) {
add(p << 1, l, mid, laz[p]);
add(p << 1 | 1, mid + 1, r, laz[p]);
laz[p] = 0;
}
}
void pushup(int p) {
max[p] = std::max(max[p << 1], max[p << 1 | 1]);
}
void build(int p, int l, int r) {
laz[p] = max[p] = 0;
if (l == r) return max[p] = cntKd[l + L - 1] - cntKd[l - 1] - (cntK[l + L - 1] - cntK[l - 1]), void();
build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
pushup(p);
}
int ask(int p, int l, int r, int L, int R) {
if (L <= l && r <= R) return max[p];
pushdown(p, l, r);
if (R <= mid) return ask(p << 1, l, mid, L, R);
if (mid + 1 <= L) return ask(p << 1 | 1, mid + 1, r, L, R);
return std::max(ask(p << 1, l, mid, L, R), ask(p << 1 | 1, mid + 1, r, L, R));
}
void add(int p, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) return add(p, l, r, v);
pushdown(p, l, r);
if (L <= mid) add(p << 1, l, mid, L, R, v);
if (mid + 1 <= R) add(p << 1 | 1, mid + 1, r, L, R, v);
pushup(p);
}
void build() {
build(1, 1, N);
}
void add(int l, int r, int v) {
add(1, 1, N, l, r, v);
}
int ask() {
return max[1];
}
void work(int i, int v) {
if (i == 0) return;
int val = -(a[i] == k - 1) - (a[i] == k + 1) + (a[i] == k) * 2;
val *= v;
add(std::max(1, i - L + 1), i, val);
}
void solve() {
cin >> n >> L >> k;
N = n << 2;
for (int i = 1; i <= (N << 1); i++) l[i] = a[i] = cntK[i] = cntKp[i] = cntKd[i] = 0;
for (int i = 1, t; i <= n; i++) {
cin >> t, t++;
l[t]++;
a[t]++, a[t + L]--;
}
for (int i = 1; i <= N + 2 * n; i++) {
a[i] += a[i - 1];
cntK[i] = cntK[i - 1] + (a[i] == k);
cntKp[i] = cntKp[i - 1] + (a[i] == k + 1);
cntKd[i] = cntKd[i - 1] + (a[i] == k - 1);
}
build();
int tl = 0, tr = 0, ans = cntK[N];
for (int i = 1; i <= N; i++) {
if (l[i] == 0) continue;
while (tl < i) work(tl++, -1);
while (tr < (i + L - 1)) work(++tr, 1);
upmax(ans, cntK[N] + ask() + cntKp[i + L - 1] - cntKp[i - 1] - (cntK[i + L - 1] - cntK[i - 1]));
}
cout << ans << endl;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem L. Label Matching
树上启发式合并的板子题。想到了启发式合并就随便写了。
感觉没什么好写的其实。
吐槽一下数据很水(或者说这种题没法造的很强)
#include <iostream>
#include <map>
#include <algorithm>
#include <vector>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n;
std::vector<int> go[kMaxN];
std::vector<int> go2[kMaxN];
int a[kMaxN], b[kMaxN];
bool ans[kMaxN];
int siz[kMaxN];
void dfs(int u, int fa = 0) {
siz[u] = 1;
for (auto v : go[u]) {
if (v == fa) continue;
go2[u].push_back(v);
dfs(v, u);
siz[u] += siz[v];
}
std::sort(go2[u].begin(), go2[u].end(), [](int i, int j) { return siz[i] > siz[j]; });
}
int moreA = 0, moreB = 0;
int cntA[kMaxN], cntB[kMaxN];
void add(int u) {
if (a[u]) {
if (cntA[a[u]] >= cntB[a[u]]) moreA++;
if (cntA[a[u]] < cntB[a[u]]) moreB--;
}
cntA[a[u]]++;
if (b[u]) {
if (cntA[b[u]] > cntB[b[u]])
moreA--;
else if (cntA[b[u]] <= cntB[b[u]])
moreB++;
}
cntB[b[u]]++;
}
void fill(int u) {
for (auto v : go2[u]) {
fill(v);
}
add(u);
}
void clear(int u) {
for (auto v : go2[u]) {
clear(v);
}
cntA[a[u]] = cntB[b[u]] = 0;
}
void work(int u) {
for (int i = 1; i < go2[u].size(); i++) {
work(go2[u][i]);
clear(go2[u][i]), moreA = moreB = 0;
}
if (go2[u].size()) work(go2[u][0]);
for (int i = 1; i < go2[u].size(); i++) fill(go2[u][i]);
add(u);
ans[u] = cntA[0] >= moreB && cntB[0] >= moreA;
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) go[i].clear(), go2[i].clear();
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
go[u].push_back(v), go[v].push_back(u);
}
dfs(1);
work(1);
clear(1), moreA = moreB = 0;
for (int i = 1; i <= n; i++) cout << ans[i];
cout << endl;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}
Problem M. Meeting for Meals
要跑两遍最短路,一次获取 从某点开始到 1 的最短路,另外一次获取 到达某点路径最短的人和路径
最大化陪伴时间等价于最小化第一次碰面时间
试图最小化碰面时间,那么最好的方式是两个人不能等待而是双向奔赴,这也是记录最早达到某点的人的必要性
任何一个人显然都至少有一个点,使得他是最早到达这个点的。记录最早到达的人为 \(f_u\)
对于任何一个人 \(i\) 来说,它显然要找到一个点 \(f_u = i\),且存在一个邻居 \(f_v \neq f_u\),这样 \(i\) 就可以和 \(f_v\) 在 \((u, v, w)\) 这条边上碰面。
判定在这条边上碰面之后是否可以在截止时间到达 \(1\) 是比较简单的,碰面的时间是 \(\frac{dis_u + dis_v + w}{2}\)。找到最小的合法碰面时间就可以了。
#include <algorithm>
#include <set>
#include <iostream>
#include <random>
#define int long long
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
int rand(int l, int r) {
static std::random_device seed;
static std::mt19937_64 e(seed());
std::uniform_int_distribution<int> rd(l, r);
return rd(e);
}
template <class type>
void upmin(type& a, const type& b) {
a = std::min(a, b);
}
template <class type>
void upmax(type& a, const type& b) {
a = std::max(a, b);
}
int t;
int n, m, k;
std::vector<int> st;
int from[kMaxN];
int dis[kMaxN], dis2[kMaxN];
std::vector<std::pair<int, int>> go[kMaxN];
void dijkstra(int* dis, int tp) {
std::fill(dis + 1, dis + 1 + n, 1e18);
std::fill(from + 1, from + 1 + n, 0);
std::set<std::pair<int, int>> s;
auto record = [&](int u, int v, int d) {
if (d >= dis[v]) return;
s.erase({dis[v], v});
dis[v] = d;
s.insert({dis[v], v});
if (tp) from[v] = from[u];
};
if (tp)
for (auto v : st) record(v, v, 0), from[v] = v;
else
record(1, 1, 0);
while (!s.empty()) {
auto [d, u] = *s.begin();
s.erase(s.begin());
for (auto [v, w] : go[u]) {
record(u, v, w + d);
}
}
}
double ans[kMaxN];
int cnt[kMaxN];
void solve() {
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) go[i].clear(), cnt[i] = 0, ans[i] = 0;
st.resize(k);
for (auto& v : st) cin >> v, cnt[v]++;
for (int i = 1, u, v, w; i <= m; i++) {
cin >> u >> v >> w;
go[u].push_back({v, w}), go[v].push_back({u, w});
}
dijkstra(dis2, 0);
dijkstra(dis, 1);
// dis 用来记录最早到达某个节点的是谁
long long tmeet = 0;
// for (int i = 1; i <= n; i++) {
// cerr << dis2[i] << ' ';
// }
// cerr << endl;
for (auto v : st) upmax(tmeet, dis2[v]);
for (int u = 1; u <= n; u++) {
int i = from[u];
for (auto [v, w] : go[u]) {
if (from[u] == from[v]) continue;
if (dis[u] + dis2[v] + w <= tmeet || dis[v] + dis2[u] + w <= tmeet) {
double tmp = (dis[u] + dis[v] + w) / 2.0;
upmax(ans[from[u]], tmeet - tmp);
upmax(ans[from[v]], tmeet - tmp);
}
}
}
for (auto v : st) {
if (cnt[v] > 1) {
printf("%.1lf ", (double)tmeet);
} else {
printf("%.1lf ", (double)ans[v]);
}
}
printf("\n");
// cerr << endl;
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> t;
while (t--) solve();
return 0;
}

浙公网安备 33010602011771号