第十一届中国大学生程序设计竞赛 女生专场(CCPC 2025 Women's Division)题解
我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?我为什么要写女生场?
很水的一场,我认为其实金线甚至可能会被冲到八九题。这场除了 \(L\) 细节比较多以外没有任何不可做题。
除了 \(I\) 和 \(L\) 以外所有题都可以在半小时内写出来。\(F\) 题想到思路了也容易写。
Problem A. 环状线
题目保证了 \(n\) 为奇数所以答案唯一。写不出退役的那种。
#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 n, s, t;
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> s >> t;
if (s < t) {
if (t - s < n - t + s) {
cout << 1 << endl;
} else {
cout << 2 << endl;
}
} else {
if (s - t < n - s + t) {
cout << 2 << endl;
} else {
cout << 1 << endl;
}
}
return 0;
}
Problem B. 爬山
简单的分层图最短路。
#include <iostream>
#include <set>
#include <queue>
#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 n, m, h;
int dis[104][(signed)(1e4) + 10];
int a[kMaxN];
std::vector<std::pair<int, int>> go[kMaxN];
void dijkstra() {
for (int i = 0; i <= h; i++) {
for (int j = 0; j <= n; j++) {
dis[i][j] = 1e18;
}
}
// dis h v
std::set<std::tuple<int, int, int>> s;
auto record = [&](int h, int v, int d) {
if (h > ::h) return;
if (d >= dis[h][v]) return;
s.erase({dis[h][v], h, v});
dis[h][v] = d;
s.insert({d, h, v});
};
record(0, 1, 0);
while (!s.empty()) {
auto [d, h, u] = *s.begin();
// cerr << "GET " << d << ' ' << h << ' ' << u << endl;
s.erase(s.begin());
for (auto [v, w] : go[u]) {
if (a[v] < a[u]) {
record(0, v, w + d);
} else {
record(h + a[v] - a[u], v, w + d);
}
}
}
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> m >> h;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
int u, v, t;
cin >> u >> v >> t;
go[u].push_back({v, t});
go[v].push_back({u, t});
}
dijkstra();
for (int i = 2; i <= n; i++) {
int min = 1e18;
for (int j = 0; j <= h; j++) upmin(min, dis[j][i]);
if (min == 1e18) min = -1;
cout << min << ' ';
}
cout << endl;
return 0;
}
Problem C. 短视频
题目描述非常的奇怪和诡异,但是读懂了不难。
#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 n, T;
int t[kMaxN], k[kMaxN];
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> T;
for (int i = 1; i <= n; i++) {
cin >> t[i] >> k[i];
}
int sum = 0;
for (int i = 1; i <= n; i++) {
if (sum + t[i] <= T) {
sum += t[i];
} else {
// 接下来考虑放几秒会导致没意思
if (sum + t[i] - T <= k[i]) {
sum += t[i];
} else {
int x = k[i] - sum + T;
if (x <= 0) x = 0;
x++;
sum += x;
}
}
}
cout << sum << endl;
return 0;
}
Problem D. 网络改造
现场居然只有四个队写出来了是我没绷住的。
发现题目的范围刚好允许我们去考虑状态压缩 DP。状态设计就比较显然了:\(dp_{s}\) 表示状态 \(s\) 为这些点被添加进来的时候的代价最小值。
考虑转移,若添加一个新的点 \(u\) 当作新的零出度点。DAG 和存在拓扑序是互为充要条件的,所以这个转移过程比较类似模拟拓扑排序,那么 \(u\) 有两个操作:
- 炸点
- 所有出边并且会和 \(s\) 中点相连的边,要么被反向要么被删除,二者等价可以视作被删了
这样才能保证新的点集一定是 DAG。
现在反思一下会发现整个转移非常类似去模拟一次拓扑排序,转移的过程就是挑选新的拓扑出队点。
这里不需要考虑炸点的后悔行为,也就是有些删边行为浪费了,比较容易证明就自己思考了。
不过判定有多少条边和点集有连是 \(O(n)\) 的,会导致 \(DP\) 的复杂度劣到 \(O(n^2 2^n)\)。这个时候跑一个二进制的前缀和优化一下可以做到 \(O(n 2^n)\)
#include <iostream>
#include <random>
#include <tuple>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e3 + 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 n, m;
int c[kMaxN];
int dp[(1 << 22) + 1];
int w[22][(1 << 22) + 1];
int a[23][23];
std::vector<std::pair<int, int>> in[kMaxN];
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> c[i], in[i].reserve(n - 1);
}
for (int i = 1; i <= m; i++) {
int u, v, a, b;
cin >> u >> v >> a >> b;
in[v].push_back({u, std::min(a, b)});
::a[u][v] = std::min(a, b);
w[u - 1][(1 << (v - 1))] = std::min(a, b);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int s = 0; s < (1 << j); s++) {
w[i][s | (1 << j)] = w[i][s] + w[i][1 << j];
}
}
}
int mx = (1 << n);
for (int s = 0; s < mx; s++) dp[s] = 1e9;
dp[0] = 0;
for (int s = 0; s < mx; s++) {
// cerr << s << ' ' << dp[s] << endl;
for (int i = 0; i < n; i++) {
if (s & (1 << i)) continue;
int to = s | (1 << i);
int s1 = 0;
// for (auto& [u, w] : in[i + 1]) {
// if (!((1 << (u - 1)) & s)) s1 += w;
// }
upmin(dp[to], w[i][s] + dp[s]);
upmin(dp[to], c[i + 1] + dp[s]);
}
}
cout << dp[mx - 1] << endl;
return 0;
}
Problem E. 购物计划
一眼题。没有意思。
由于满减行为是可以不断翻倍的,所以可以通过带根号的算法处理出每个整数价格时的最多满减,记作 \(d_i\)。
通过上述操作可以处理出:一段整数区间内的最小价格,也就是 \(w_i = i - d_i\) 的区间最小值,这里用个 ST 表之类的写一下就好了。
每次询问相当于在一个实数区间内挑选最小值,事实上我们可以拆成两个询问区间,其中一个双边界为整数,另外一个区间包括小数边界和整数边界。
有小数边界的区间由于小于一可以直接找到满减,整数边界的区级可以用 ST 表查出来最小价格,二者做个比较就算出来了。
很简单的题目,但是却成为了这场的金牌题,很莫名其妙。
#include <iostream>
#include <algorithm>
#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 n, m;
int a[kMaxN], b[kMaxN];
int w[kMaxN], p[kMaxN], q[kMaxN];
int dec[kMaxN];
int cost[21][kMaxN];
int BIN[kMaxN];
int mincost(int l, int r) {
int t = BIN[r - l + 1];
return std::min(cost[t][l], cost[t][r - (1 << t) + 1]);
}
signed main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> m;
for (int i = 1, a, b; i <= m; i++) {
cin >> a >> b;
dec[a] = std::max(dec[a], b);
}
BIN[2] = 1;
for (int i = 3; i < kMaxN; i++) {
BIN[i] = BIN[i >> 1] + 1;
}
for (int i = 1; i < kMaxN; i++) {
for (int j = 1; i * j < kMaxN; j++) {
upmax(dec[i * j], dec[i] * j);
}
}
for (int i = 1; i < kMaxN; i++) {
upmax(dec[i], dec[i - 1]);
}
for (int i = 1; i < kMaxN; i++) {
cost[0][i] = i - dec[i];
}
for (int t = 1; t < 21; t++) {
for (int l = 1; l + (1 << t) - 1 < kMaxN; l++) {
cost[t][l] = std::min(cost[t - 1][l], cost[t - 1][l + (1 << (t - 1))]);
}
}
for (int i = 1, w, p, q; i <= n; i++) {
cin >> w >> p >> q;
p *= w;
int dec1 = dec[p / q], s = p / q, t = w;
if (p % q) s++;
int min = mincost(s, t);
p = p - dec[p / q] * q;
if (min * q <= p) {
cout << min << ' ' << 1 << endl;
} else {
auto gcd = std::__gcd(p, q);
p /= gcd, q /= gcd;
cout << p << ' ' << q << endl;
}
}
return 0;
}
Problem F. 丝之歌
一开始把这个题强化了,以为小怪死了不会复活甚至以为这个题不可做……
题目生怕你想不到矩阵优化转移都给你把转移矩阵甩脸上了……要是还想不到矩阵乘法,出门左传高职赛。
记 \(dp_i\) 为还有 \(i\) 个念珠没有被兑换的时候的概率。每次转移的时候只需要考虑是否是一命通关,这个判定是否一命通关通过一大坨矩阵乘法优化转移即可。
每次转移的时候可以累计念珠串的期望,注意最后一次转移不需要算到念珠串期望去了。
但是你不优化矩阵乘法,复杂度是 \(O(ntc^3 \log(1e9))\) 这个级别的,非常的巨大恐怖吓人。
所以我们要抛弃矩阵快速幂,提前存下来每个转移矩阵的 \(2^k\) 次幂。同时注意到转移的时候有一个运算矩阵只是一个向量,可以将矩阵乘法复杂度压倒 \(c^2\)。
这样和矩阵相关的复杂最后可以处理成 \(O(ntc^2 \log(1e9))\),复杂度就对了。
同时题目还有两个小的常数优化:
- 参与运算的全是下三角矩阵,下三角矩阵乘下三角一定还是下三角,所以矩乘的时候 \(i \leq j \leq k\),优化出 \(\frac{1}{6}\) 的常数
- 矩阵运算减少取模运算,优化方式看代码
#include <cassert>
#include <iostream>
#include <random>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e3 + 100;
const int MOD = 998244353;
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 inc(int x, int y) {
return (x + y >= MOD ? x + y - MOD : x + y);
}
int dec(int x, int y) {
return (x - y < 0 ? x - y + MOD : x - y);
}
int mul(long long x, long long y) {
x %= MOD, y %= MOD;
return 1ll * x * y % MOD;
}
using ull = unsigned long long;
struct matrix {
int a[11][11];
int* operator[](int id) { return a[id]; }
matrix operator*(matrix& b) {
matrix c;
for (int i = 0; i < 11; i++) {
for (int j = 0; j <= i; j++) {
ull tmp = 0;
for (int k = j; k <= i; k++) {
tmp += (ull)a[i][k] * b[k][j];
}
c[i][j] = tmp % MOD;
}
for (int j = i + 1; j < 11; j++) c[i][j] = 0;
}
return c;
}
} mon[kMaxN], f[kMaxN][30];
int n, m, a, b, c;
int w[kMaxN];
int pow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = mul(ans, x);
x = mul(x, x), p >>= 1;
}
return ans;
}
matrix pow(matrix x, int p) {
matrix ans;
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 11; j++) ans[i][j] = i == j;
}
while (p) {
if (p & 1) ans = ans * x;
x = x * x, p >>= 1;
}
return ans;
}
// i 个念珠的概率
int dp[2][kMaxN];
int cur = 0;
int ans = 0;
std::vector<int> list;
void calc(int id) {
cur ^= 1;
int t, lst = cur ^ 1;
cin >> t;
matrix begin;
for (int i = 0; i <= c; i++) begin[0][i] = i == c;
long long all = 0;
for (int i = 1; i <= t; i++) {
int type, cnt;
cin >> type >> cnt;
for (int t = 0; t < 30; t++) {
if (((cnt >> t) & 1) ^ 1) continue;
auto tmp = f[type][t];
// auto tmp = pow(mon[type], cnt);
// begin = begin * tmp;
// 手动展开转移
for (int j = 0; j <= c; j++) {
ull s = 0;
for (int k = 0; k <= c; k++) {
s += 1ll * begin[0][k] * tmp[k][j];
// begin[0][j] =
}
begin[0][j] = s % MOD;
}
}
all += 1ll * cnt * w[type];
}
int win = 0;
for (int i = 1; i <= c; i++) {
win = inc(win, begin[0][i]);
if (begin[0][i] < 0 || win < 0) {
exit(-1);
}
}
// assert(inc(win, begin[0][0]) == 1);
// for (int i = 0; i < a; i++) {
// dp[cur][i] = 0;
// }
// cerr << "END CALC " << endl;
if (id != n) {
int p, nxt, val;
std::vector<int> tmp;
for (auto i : list) {
p = mul(win, dp[lst][i]), nxt = (i + all) % a;
if (dp[cur][nxt] == 0 && p != 0) tmp.push_back(nxt);
dp[cur][nxt] = inc(dp[cur][nxt], p);
ans = inc(ans, mul(p, (i + all) / a));
p = mul(dec(1, win), dp[lst][i]), nxt = all % a;
if (dp[cur][nxt] == 0 && p != 0) tmp.push_back(nxt);
dp[cur][nxt] = inc(dp[cur][nxt], p);
ans = inc(ans, mul(p, all / a));
dp[lst][i] = 0;
}
list.swap(tmp);
// cerr << list.size() << endl;
} else {
ans = inc(mul(ans, b), all % MOD);
for (int i = 0; i < a; i++) {
ans = inc(ans, mul(win, mul(dp[lst][i], i)));
}
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> m >> a >> b >> c;
for (int i = 1; i <= m; i++) {
cin >> w[i];
}
for (int i = 1; i <= m; i++) {
mon[i][0][0] = 1;
for (int x = 1; x <= c; x++) {
int sum = 0;
for (int y = 0; y <= x; y++) {
cin >> mon[i][x][y];
sum = inc(sum, mon[i][x][y]);
}
sum = pow(sum, MOD - 2);
int sum2 = 0;
for (int y = 0; y <= x; y++) {
mon[i][x][y] = mul(mon[i][x][y], sum);
sum2 = inc(sum2, mon[i][x][y]);
}
}
f[i][0] = mon[i];
for (int j = 1; j < 30; j++) f[i][j] = f[i][j - 1] * f[i][j - 1];
}
dp[cur][0] = 1, list.push_back(0);
for (int i = 1; i <= n; i++) {
calc(i);
}
cout << ans << endl;
cerr << list.size() << endl;
return 0;
}
Problem G. 最大公约数
写不出退役。
#include <iostream>
#include <random>
#include <vector>
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 n, k;
bool no[kMaxN];
void sieve() {
for (int i = 2; i < kMaxN; i++) {
if (!no[i])
for (int j = 2; j * i < kMaxN; j++) {
no[i * j] = true;
}
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> k;
sieve();
std::vector<int> ans;
for (int i = 1; i <= n && ans.size() < k; i++) {
if (!no[i]) ans.push_back(i);
}
if (ans.size() != k) return cout << "NO" << endl, 0;
cout << "YES" << endl;
for (auto& i : ans) {
cout << i << ' ';
}
cout << endl;
return 0;
}
Problem H. 缺陷解码器
可以看官方题解,写的很好,是一个很好的期望 DP 入门题。
我这里只讲一点碎碎念。一个字符串注意到我们只会关系它本质不同的字符个数,新加进来的到底是多少我们并不怎么关心,基于这个思路去优化状态设计和转移。
#include <algorithm>
#include <iostream>
#include <random>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
const int MOD = 1e9 + 7;
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 inc(int x, int y) {
return (x + y >= MOD ? x + y - MOD : x + y);
}
int dec(int x, int y) {
return (x - y < 0 ? x - y + MOD : x - y);
}
int mul(int x, int y) {
return 1ll * x * y % MOD;
}
int pow(int x, int p) {
int ans = 1;
while (p) {
if (p & 1) ans = mul(ans, x);
x = mul(x, x), p >>= 1;
}
return ans;
}
int n, m;
bool check(const std::vector<int>& str) {
if (str.size() < 2) return true;
bool check1 = true, check2 = true;
int mid = str.size() >> 1;
for (int i = 0; i < mid; i++) {
check1 &= str[i] == str[str.size() - 1 - i];
check2 &= str[i] == str[mid + i + (str.size() & 1)];
}
return !(check1 | check2);
}
int inv;
bool flag = false;
std::pair<int, int> dfs(std::vector<int>& str, int max) {
if (!check(str)) return {1, 0};
if (str.size() == n) {
flag = true;
return {0, 0};
}
int a = 0, b = 0;
str.push_back(0);
for (int i = 1; i <= max; i++) {
str.back() = i;
auto [na, nb] = dfs(str, max);
a = inc(a, na), b = inc(b, nb);
}
if (max + 1 <= m) {
str.back() = max + 1;
auto [na, nb] = dfs(str, max + 1);
a = inc(a, mul(na, m - max)), b = inc(b, mul(nb, m - max));
}
a = mul(a, inv), b = inc(1, mul(b, inv));
str.pop_back();
return {a, b};
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
// 容易发现字符集超过 n 的时候无意义
cin >> n >> m;
inv = pow(m, MOD - 2);
std::vector<int> tmp;
auto [a, b] = dfs(tmp, 0);
if (!flag) {
return cout << -1 << endl, 0;
}
auto ans = mul(b, pow(dec(1, a), MOD - 2));
cout << ans << endl;
return 0;
}
Problem I. 调色滤镜
题目中的滤镜就是置换操作,置换这个行为是不一定存在逆的而且不满足交换律,单位元是 0 1 2 3 4 5 ...
包含关系一定会构成一个树,这个树可以通过扫描线构造出来。扫描线的时候由于保证两个线段只会包含而不是相交,可以使用的数据结构非常多,简单一点直接写个 set 都是可以的。但是我却写了一个线段树去构建关系树
我们在对关系树进行遍历的时候同步维护一个线段树,线段树的节点值就是置换,线段树叶子节点要初始化成单位元,并且规定 pushup 操作(也就是区间并)是以右儿子为置换规则去置换左儿子。
设定节点 \(i\) 为矩形 \(i\) 对应的置换关系,这样就可以处理好置换添加顺序的问题,同时在关系树上转移的时候维护一下这个线段树就好。
当某个节点 \(i\) 出栈的时候强行将 \(i\) 初始化为单位元。
比较有意思的一个题。
如果题目保证置换操作存在逆,也就是每种元素只出现一次的话,其实是可以允许矩形是相交关系的。但是这样写会变成比较板的扫描线,可以当个 idea 去恶心别人。
#include <algorithm>
#include <cassert>
#include <iostream>
#include <set>
#include <random>
#include <vector>
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);
}
class SegmentTree {
struct node {
int l, r, val, laz;
node* son[2];
}* root;
void init(node* p, int s) { p->val = p->laz = s; }
void pushdown(node* p) {
if (~p->laz) init(p->son[0], p->laz), init(p->son[1], p->laz), p->laz = -1;
}
void build(node* p, int l, int r) {
p->l = l, p->r = r, p->laz = -1, p->val = 0;
if (l == r) return p->val = 0, void();
int mid = (l + r) >> 1;
build(p->son[0] = new node, l, mid), build(p->son[1] = new node, mid + 1, r);
}
void init(node* p, int l, int r, int s) {
if (l <= p->l && p->r <= r) return init(p, s);
pushdown(p);
if (p->son[0]->r >= l) init(p->son[0], l, r, s);
if (p->son[1]->l <= r) init(p->son[1], l, r, s);
}
int ask(node* p, int x) {
if (p->l == p->r) return p->val;
pushdown(p);
if (p->son[0]->r >= x) return ask(p->son[0], x);
return ask(p->son[1], x);
}
public:
int ask(int x) { return ask(root, x); }
void build(int n) { build(root = new node, 1, n); }
void init(int l, int r, int s) { init(root, l, r, s); }
} tree;
class SegmentTree2 {
struct node {
int l, r, f[10];
node* son[2];
}* root;
void pushup(node* p) {
for (int c = 0; c < 10; c++) {
p->f[c] = p->son[1]->f[p->son[0]->f[c]];
}
}
void build(node* p, int l, int r) {
p->l = l, p->r = r;
for (int i = 0; i < 10; i++) p->f[i] = i;
if (l == r) return void();
int mid = (l + r) >> 1;
build(p->son[0] = new node, l, mid), build(p->son[1] = new node, mid + 1, r);
pushup(p);
}
void change(node* p, int* f) {
for (int i = 0; i < 10; i++) p->f[i] = f[i];
}
void change(node* p, int l, int r, int* f) {
if (l <= p->l && p->r <= r) return change(p, f);
if (p->son[0]->r >= l) change(p->son[0], l, r, f);
if (p->son[1]->l <= r) change(p->son[1], l, r, f);
pushup(p);
}
public:
int* ask() { return root->f; }
void build(int n) { build(root = new node, 1, n); }
void change(int l, int r, int* f) { change(root, l, r, f); }
} tree2;
int n, q;
int xl[kMaxN], xr[kMaxN], yl[kMaxN], yr[kMaxN];
int f[kMaxN][10];
int x[kMaxN], y[kMaxN], c[kMaxN];
int ans[kMaxN];
// 从属于哪个矩形
std::vector<int> bel[kMaxN];
// 矩形之间的父子关系
int fa[kMaxN];
int tot = 0;
int l[kMaxN], r[kMaxN];
std::vector<int> uno;
int map(int y) {
return std::lower_bound(uno.begin(), uno.end(), y) - uno.begin() + 1;
}
std::vector<int> go[kMaxN];
void dfs(int u) {
if (u) tree2.change(u, u, f[u]);
auto sf = tree2.ask();
for (auto id : bel[u]) {
ans[id] = sf[c[id]];
}
for (auto v : go[u]) dfs(v);
if (u) tree2.change(u, u, f[0]);
}
struct node {
// type 0: insert
// type 1: query
// type 2: erase
int x, type, id;
bool operator<(const node& b) const {
if (x != b.x) return x < b.x;
if (type != b.type) return type < b.type;
return id < b.id;
}
};
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n >> q;
// 第一个节点表示x坐标,第二个用来标记是查询点还是矩阵
std::set<node> s;
// std::set<int> inset;
for (int i = 1; i <= n; i++) {
cin >> xl[i] >> xr[i] >> yl[i] >> yr[i];
for (int j = 0; j <= 9; j++) cin >> f[i][j];
uno.push_back(yl[i]), uno.push_back(yr[i]);
s.insert({xl[i], 0, i});
s.insert({xr[i], 2, i});
}
for (int i = 1; i <= q; i++) {
cin >> x[i] >> y[i] >> c[i];
uno.push_back(y[i]);
s.insert({x[i], 1, i});
}
std::sort(uno.begin(), uno.end()), uno.erase(std::unique(uno.begin(), uno.end()), uno.end());
tree.build(uno.size());
for (auto [x, type, id] : s) {
if (type == 1) {
bel[tree.ask(map(y[id]))].push_back(id);
// bel[id] = tree.ask(map(y[id]));
} else if (type == 2) {
tree.init(map(yl[id]), map(yr[id]), fa[id]);
} else if (type == 0) {
fa[id] = tree.ask(map(yl[id]));
assert(fa[id] == tree.ask(map(yr[id])));
tree.init(map(yl[id]), map(yr[id]), id);
}
}
for (int i = 1; i <= n; i++) {
go[fa[i]].push_back(i);
}
for (int i = 0; i < 10; i++) f[0][i] = i;
tree2.build(n);
dfs(0);
for (int i = 1; i <= q; i++) {
cout << ans[i] << endl;
}
return 0;
}
Problem J. 后鼻嘤
写不出退役题。
#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 main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
std::string str;
while (cin >> str) {
if (str.back() == 'n') str += "g";
cout << str << ' ';
}
return 0;
}
Problem K. 左儿子右兄弟
手玩会发现,某个节点的儿子一定会构成一条往右下方走的链,而且统计子树大小的时候靠后的节点会被计算多次。
基于这个思路会有显然的贪心想法,也就是子树越大的尽量在这条链的前面,这样就解决了计数问题。
至于方案数的问题……都想到了怎么计数真的想不出这个?
#include <iostream>
#include <algorithm>
#include <random>
#include <vector>
using std::cerr;
using std::cin;
using std::cout;
const char endl = '\n';
const int kMaxN = 1e6 + 100;
const int MOD = 998244353;
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 inc(int x, int y) {
return (x + y >= MOD ? x + y - MOD : x + y);
}
int mul(int x, int y) {
return 1ll * x * y % MOD;
}
int dec(int x, int y) {
return (x - y >= 0 ? x - y : x - y + MOD);
}
int n;
std::vector<int> go[kMaxN];
long long ans = 0;
int siz[kMaxN];
int cnt = 1;
int fact[kMaxN];
void dfs(int u) {
siz[u] = 1;
for (auto v : go[u]) {
dfs(v);
siz[u] += siz[v];
}
int sum = siz[u] - 1;
std::sort(go[u].begin(), go[u].end(), [](int i, int j) { return siz[i] > siz[j]; });
for (int i = 0; i < go[u].size(); i++) {
ans += sum;
sum -= siz[go[u][i]];
}
for (int i = 0, j; i < go[u].size(); i = j) {
for (j = i; j < go[u].size() && siz[go[u][i]] == siz[go[u][j]]; j++);
cnt = mul(cnt, fact[j - i]);
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false), cout.tie(nullptr), cerr.tie(nullptr);
cin >> n;
fact[0] = 1;
for (int i = 1; i < kMaxN; i++) {
fact[i] = mul(fact[i - 1], i);
}
for (int i = 2, f; i <= n; i++) {
cin >> f, go[f].push_back(i);
}
dfs(1);
cout << ans + n << endl;
cout << cnt << endl;
return 0;
}
Problem L. 挑战多边形
这套题里为数不多有点难度的题目。但是抽象的范围让这道题变的比较搞笑。
将所有可能的有向边进行排序,然后钦定某个点一定会出现在凸包里面。
由于被删除的点数量不超过 \(k\),则前 \(k + 1\) 中某个节点一定会在答案凸包里。
记录 DP 状态 \(f_{i, j}\) 为,从 \(p\) 开始,以极座标排序后,边连到 \(i\) 点并且凸包边数为 \(j\) 的最大权。转移的时候只需要考虑第二维的最大 \(k\) 个即可,这个性质的正确性也是基于被删点不超过 \(k\) 这个性质。
不想写了以后有时间再补。

浙公网安备 33010602011771号