The 2024 CCPC National Invitational Contest (Changchun) , The 17th Jilin Provincial Collegiate Programming Contest 题解
B
动态规划
解题思路
一眼 dp,主要就是怎么去转移。
我们定义 \(dp[u][0/1]\) 为在整个 dfs 序中节点 \(u\) 在偶数位/奇数位时,子树 \(u\) 的最大价值。
为了简便描述,我们将 “\(u\) 在偶数位” 记作 “从 0 进入 \(u\)”,将 “\(u\) 在奇数数位” 记作 “从 1 进入 \(u\)”。
显然的我们要额外维护出每颗子树的价值,因为对于大小为奇数的子树,若我们从 0 进入这颗子树,则就要从 1 进入下一颗子树;对于大小为偶数的子树,若我们从 0 进入这颗子树,则就要从 0 进入下一颗子树。
假设 \(u\) 有儿子节点 \(v\), 且 儿子节点的大小是 \(siz(v)\)。
考虑特殊情况:当所有 \(siz(v)\) 都是偶数时,有
其余的情况就是存在 \(siz(v)\) 是奇数的情况,此时对于所有 \(siz(v)\) 为偶数的子树,我们可以任意从 0 或者从 1 进入。对 \(u\) 的贡献是:
对于 \(siz(v)\) 是奇数的子树,应该选择一半从 0 进入,另一半从 1 进入,若有奇数个这样的子树,则剩下的一颗从哪里进入取决于从哪里进入 \(u\)(如果从 0 进入 \(u\) 就从 \(1\) 进入剩下的那颗)。假设我们要对 \(dp[u][bit]\) 进行转移,且根据上面的分析我们知道了要从 1 进入 \(x\) 颗大小为奇数的子树,要从 0 进入 \(y\) 颗大小为偶数的子树,则我们要考虑那些子树从 1 进入和那些子树从 0 进入(因为从 1 进入就取 \(dp[v][1]\),从 0 进入就取 \(dp[v][0]\))。
这就像问你有一个二元组数组 \(A\),从每个二元组里选数,要求有 \(x\) 个二元组选前面一个数, \(y\) 个二元组选后面一个,能够选到的最大的和是多少。按二元组内的差排序然后贪心地求就好了。解决了这个问题我们就解决了大小为奇数地子树的转移,也就解决了问题。
CODE
std::vector<int> g[N + 1], a(N + 1, 0);
std::array<i64, 3> dfs(int cur, int fa) {
int siz = 1;
std::vector<std::array<i64, 2>> obb, eve;
for (auto &to : g[cur]) {
if (to == fa) {
continue;
}
auto [v0, v1, sizt] = dfs(to, cur);
if (sizt & 1) {
obb.push_back({ v0, v1 });
}
else {
eve.push_back({ v0, v1 });
}
siz += sizt;
}
i64 v0 = 0, v1 = 0;
if (obb.size() == 0) {
for (auto &[vv0, vv1] : eve) {
v0 += vv1, v1 += vv0;
}
}
else {
// 大小为偶数子树可以任选两个值
for (auto &[vv0, vv1] : eve) {
v0 += std::max(vv0, vv1);
}
v1 = v0;
i64 tmp = 0;
for (auto &[vv0, vv1] : obb) {
tmp += vv0;
}
std::sort(obb.begin(), obb.end(), [](auto &u, auto &v) {
return u[1] - u[0] > v[1] - v[0];
});
for (int i = 0; i + i < obb.size(); i++) {
tmp += obb[i][1] - obb[i][0];
}
v1 += tmp, v0 += tmp;
if (obb.size() & 1) {
int m = obb.size() >> 1;
v1 += obb[m][0] - obb[m][1];
}
}
return { v0 + a[cur], v1, siz };
}
void solve() {
int n = 0;
std::cin >> n;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
g[i].clear();
}
for (int i = 0; i < n - 1; i++) {
int u = 0, v = 0;
std::cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
std::cout << dfs(1, 0)[1] << '\n';
}
D
计算几何 扫描线
解题思路
题解是说随机选两个点重复 \(k\) 遍就好了,我一开始是这么想的但是概率算错了以为是不可行的。于是自己想的是遍历凸包上的边,将这些边的斜率视为 \(k\) 条线的斜率看合不合法,具体的方法是,对于一个点,根据斜率算截距,所有截距相同的点就认为在一条直线上:
CODE
std::array<int, 2> v(std::array<int, 3> u, std::array<int, 3> v) {
return { v[0] - u[0], v[1] - u[1] };
}
i64 cross(std::array<int, 2> u, std::array<int, 2> v) {
// x1y2 - x2y1
return 1ll * u[0] * v[1] - 1ll * v[0] * u[1];
}
void solve() {
int n = 0, k = 0;
std::cin >> n >> k;
std::vector p(n, std::array<int, 3>{});
std::unordered_map<double, std::vector<int>> mp;
int idx = 0;
for (auto &[x, y, id] : p) {
std::cin >> x >> y;
id = idx++;
mp[x].push_back(id);
}
auto out = [](std::unordered_map<double, std::vector<int>> &mp) -> bool {
bool ok = true;
for (auto &[b, v] : mp) {
if (v.size() == 1) {
ok = false;
break;
}
}
if (ok) {
for (auto &[b, v] : mp) {
std::cout << v.size() << ' ';
for (auto &id : v) {
std::cout << id + 1 << ' ';
}
std::cout << '\n';
}
}
return ok;
};
// 特殊情况,k 条竖线x
if (mp.size() == k && out(mp)) {
return;
}
std::sort(p.begin(), p.end(), [](auto &u, auto &v) {
if (u[0] == v[0]) {
return u[1] < v[1];
}
else {
return u[0] < v[0];
}
});
// 求凸包
std::vector stk(2 * n + 5, 0);
int top = 0;
for (int i = 0; i < n; i++) {
while (top >= 2 && cross(v(p[stk[top - 1]], p[i]), v(p[stk[top - 2]], p[stk[top - 1]])) >= 0) {
top--;
}
stk[top++] = i;
}
int siz = top;
for (int i = n - 2; i >= 0; i--) {
while (top > siz && cross(v(p[stk[top - 1]], p[i]), v(p[stk[top - 2]], p[stk[top - 1]])) >= 0) {
top--;
}
stk[top++] = i;
}
for (int i = 1; i < top; i++) {
auto [x1, y1, id1] = p[stk[i - 1]];
auto [x2, y2, id2] = p[stk[i]];
if (x1 == x2) {
continue;
}
double kk = (1.0 * (y1 - y2)) / (1.0 * (x1 - x2));
mp.clear();
for (int i = 0; i < n; i++) {
double b = 1.0 * p[i][1] - 1.0 * p[i][0] * kk;
mp[b].push_back(p[i][2]);
}
if (mp.size() == k && out(mp)) {
break;
}
}
return;
}
E
观察 二维偏序
解题思路
首先根据题目的式子,我们可以将两点之间有边的条件变为:\((a_i - i) \lesseqgtr (a_j - j) \And (i - b_i) \lesseqgtr (j - b_j)\)。于是问题就变成了二维偏序问题:
CODE
void solve()
{
int n = 0;
std::cin >> n;
std::vector a(n, std::array<int, 2>{});
for (int i = 0; i < n; i++) {
std::cin >> a[i][0] >> a[i][1];
a[i][0] -= i, a[i][1] = i - a[i][1];
}
std::sort(a.begin(), a.end(), [](auto &u, auto &v) { return u[0] != v[0] ? u[0] < v[0] : u[1] < v[1]; });
std::stack<int> stk;
for (auto &[A, B] : a) {
if (stk.empty() || B < stk.top()) {
stk.push(B);
}
else {
int mn = stk.top();
while (not stk.empty() && B >= stk.top()) {
stk.pop();
}
stk.push(mn);
}
}
std::cout << stk.size() << '\n';
}
F
解题思路
对于特定的玩家 \(i\),我们只用看当局面对他最有利时能不能当冠军就可以了,最有利的局面是指,对于他能拿的分最多的场次,让他全部拿完,其他的场就尽量平局分配。但是注意存在一些场,使得 \(i\) 就算拿完了所有的分也仍然超越不了对面,这样的场即使 \(i\) 拿的分是最多的,我们也不去用这个分更新最大值。
于是我们只需要用一个 multiset 来维护所有分数平均分配的场。在确定 \(i\) 时,我们枚举他参与的所有场,并且在 multiset 里删去大于等于当前最大值的场,具体看代码就好了:
CODE
void solve()
{
int n = 0, m = 0;
std::cin >> n >> m;
std::vector py(n + 1, std::vector<int>{});
std::vector con(m + 1, std::array<int, 5>{});
std::multiset<int> s;
for (int i = 1; i <= m; i++) {
int a = 0, b = 0, x = 0, y = 0, z = 0;
std::cin >> a >> b >> x >> y >> z;
con[i] = { a, b, x, y, z };
s.insert(std::max({ x, y, x + y + z + 1 >> 1 }));
py[a].push_back(i);
py[b].push_back(i);
}
std::vector<int> ans;
for (int i = 1; i <= n; i++) {
int sc = -1;
std::vector<int> val;
for (int j = 0; j < py[i].size(); j++) {
auto &[a, b, x, y, z] = con[py[i][j]];
// 这一场在 s 中的值是 v
int v = std::max({ x, y, x + y + z + 1 >> 1 });
if (i == a) {
if (x + z > y) {
sc = std::max(sc, x + z);
val.push_back(v);
// 注意不能写 s.erase(v)
// 区别是,上面是删去所有值为 v 的,下面是只删一个
s.erase(s.find(v));
}
}
else {
if (y + z > x) {
sc = std::max(sc, y + z);
val.push_back(v);
s.erase(s.find(v));
}
}
}
if (s.empty() || sc > *s.rbegin()) {
ans.push_back(i);
}
for (auto v : val) {
s.insert(v);
}
}
std::cout << ans.size() << '\n';
for (auto &i : ans) {
std::cout << i << ' ';
}
std::cout << '\n';
return;
}
浙公网安备 33010602011771号