ACM散题习题库5【持续更新】
终于更新到5了,但是发现并不是做过的题仍然记得,所以现在应该着重记录一些相对简单且模板的题目了。
501. H - Clock HDU - 6551【环上点覆盖 问题】
题意:给你一个环[0,N-1],和一个起始点S,同时还有n个在环上的点,请你求出最短的时间从S出发,去覆盖这n个点。
解决这个环问题的关键在于拆环。拆环的关键在于确定拆环的点,然后把这个点当作原点O。然后就可以从序列的角度去写for循环了,对我来说这样容易理解。
定义到第个点的顺时针距离为,我们把n个点根据排序,就可以得到一个序列啦!然后就枚举和这两个点作为两个端点,到的距离就是,到的距离是。那么答案就是: 。然后记得特判一下首尾就行了。
502. J - Tangram HDU - 6553【小学奥数 + 切蛋糕题目】【题解】
理解:在一个空白平面,一条直线把平面划分为2,平面数量+1。
然后以后的每一条直线都和前面的k-1直线相交,平面数量加k。主要想清楚为什么一条直线和其它k条直线相交会多出k+1个平面,这道题就好做了。
503. 2019CCPC女生赛-Union【搜索计数】
题目给定了7个条件,直接考虑这些条件确实不好计数。所以考虑枚举:
① a,b,c中重合了多少数;
② a,b中重合了多少数;
③ a,c中重合了多少数;
④ b,c中重合了多少数;
⑤ a,b,c各出现了多少个数。
枚举完之后再乘个组合系数就行了。
504. 2019CCPC女生赛-String【贪心 + DP】
空间复杂度有点大?, 其中。【这道题的数据貌似有点问题,迷惑,我特判了ans=0的情况竟然wa了,不特判居然过了】
因为题目说至多k段,所以贪心地选长度为L来修改是合理的。定义状态:表示做了前i位,最后一个字符是c,有j段的答案。转移方式为:
查看代码
const int maxn = 1e5 + 3, inf = 0x3f3f3f3f;
int n, l, k, dp[maxn][26][11], pre[maxn][26][11], suf[maxn][26][11];
char str[maxn];
int main() {
ios_fast;
cin >> n >> l >> k >> str + 1;
memset(dp, 0x3f, sizeof(dp));
for (int i = 0; i < 26; i++) {
dp[1][i][1] = (i == str[1] - 'a');
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i && j <= k; j++) {
// prework
pre[i - 1][0][j] = dp[i - 1][0][j];
suf[i - 1][25][j] = dp[i - 1][25][j];
for (int c = 1; c < 26; c++)
pre[i - 1][c][j] = min(dp[i - 1][c][j], pre[i - 1][c - 1][j]);
for (int c = 24; ~c; c--)
suf[i - 1][c][j] = min(dp[i - 1][c][j], suf[i - 1][c + 1][j]);
// trans
int x = max(i - l, 0);
for (int c = 0; c < 26; c++) {
if (c == str[i] - 'a') {
dp[i][c][j] = dp[i - 1][c][j];
if (j > 1) { // 特判无消耗转移
int t0 = c ? pre[x][c - 1][j - 1] : inf;
int t1 = c < 25 ? suf[x][c + 1][j - 1] : inf;
dp[i][c][j] = min({dp[i][c][j], t0, t1});
}
} else {
if (x) {
dp[i][c][j] = dp[x][c][j] + 1;
if (j > 1) {
int t0 = c ? pre[x][c - 1][j - 1] : inf;
int t1 = c < 25 ? suf[x][c + 1][j - 1] : inf;
dp[i][c][j] = min(t0, t1) + 1;
}
} else {
dp[i][c][1] = 1;
}
}
}
}
}
int ans = inf;
for (int i = 0; i < 26; i++)
for (int j = 0; j <= k; j++) ans = min(ans, dp[n][i][j]);
cout << ans << endl;
}
505. 2022年SCUT-新生杯B题 - AC! 【区间DP / 反悔贪心】
题意:给你一个串S,只包含a、c、p三种字符,有两种操作,每次你可以选择一种操作来执行:
- 你选择S中的子串 'ac' ,然后删掉它。
- 你选择S中的子串 'cp', 然后删掉它。
每次删掉两个字符,串都会重新拼接起来。你的任务是将整个串删空,但是直接删可能无法将整个串删空,所以你可以在字符串S的任意一个地方插入一个任意的字符。现在问你最少插入多少个字符,使得整个串被删掉。原题,但是实际上贪心可以通过。
(方法1:区间DP)
初始状态:.
转移方程:有两种转移方式
- 如果 可以有
- 然后都要进行区间DP的转移方式:
(方法2:反悔贪心)
506. E.Master of Subgraph【点分治优化 + bitset表示状态的DP】【查看聚聚的题解】
这道题一点头绪都没有。。。主要是连搜索都不会写,然后DP的状态根本想不出来。【这题数据范围n=3000,但是数据中边集好像出现了大于3000的点,开成6000就能过了】
考虑一个的暴力,我们枚举每一个点作为连通块的必过点Root(连通块必须包含这个节点)。定义`bitset<100000> dp[x]`表示dfs到x节点(以及一部分x的孙子节点)的时候,包含Root和x的联通块的权值。【这个"dfs到x节点"的说法有点巧妙,之前都没见过这样的】
那么从x再dfs到某个儿子节点v的时候,dp[x]应该转移给dp[v],所以`dp[v] = dp[x] << w[v]`。如果v是一个叶子节点的话,dp[v]就直接是v节点的答案了。那么现在从v退栈到x,应该让`dp[x]|=dp[v]`。这样的话,就不需要做很多次背包/卷积了。因为在枚举x的下一个儿子节点`nxt_v`的时候,就会有`dp[nxt_v]=dp[x]<<w[nxt_v]`,就达到了背包的组合效果(x, 感觉很神奇。
现在复杂度是,实际上我们枚举了很多重复的状态(很多重复的连通块)。
所以我们考虑一个Root被选了之后,就把它的所有子树单独求解,这样就不会重复计算了。然后想到这里应该能想到点分治优化这个分治过程。最后复杂度就是\(O(\frac{nlogn*100000}{64})。
点分治代码
#include <bits/stdc++.h>
using namespace std;
#define ios_fast ios::sync_with_stdio(0), cin.tie(0), cout.tie(0)
const int maxn = 3e3 + 11;
// 输入信息
int n, m, w[maxn], head[maxn], etop;
bitset<100005> dp[maxn], ans;
vector<vector<int>> e;
// 点分治信息
int root, mx_son_sz[maxn], sz[maxn], vis[maxn];
void addEdge(int x, int y) {
e[++etop] = {y, head[x]}, head[x] = etop;
e[++etop] = {x, head[y]}, head[y] = etop;
}
void find_root(int x, int p, int tot_sz) { // totsz不可以加引用
mx_son_sz[x] = 0, sz[x] = 1;
for (const int & v : e[x]) {
if (vis[v] || v == p) continue;
find_root(v, x, tot_sz);
sz[x] += sz[v];
if (mx_son_sz[x] < sz[v]) mx_son_sz[x] = sz[v];
}
mx_son_sz[x] = max(mx_son_sz[x], tot_sz - sz[x]);
if (root == -1 || mx_son_sz[x] < mx_son_sz[root]) root = x;
}
void dfs(int x, int p) {
sz[x] = 1; // 这里重新计算sz也行,不算也行,不算的话,只有父节点的sz是错误的,但是也不影响复杂度.[跳到line-51:①]
dp[x] = dp[p] << w[x];
for (const int & v : e[x]) {
if (vis[v] || v == p) continue;
dfs(v, x);
sz[x] += sz[v];
dp[x] |= dp[v];
}
}
void Work(int x, int tot_sz) {
root = -1;
find_root(x, 0, tot_sz);
vis[root] = true;
dfs(root, 0);
ans |= dp[root];
x = root; // 注意赋值记录root
for (const int & v : e[x]) {
if (!vis[v]) Work(v, sz[v]); // 这里①:如果上面不算,只有x的父节点的sz是错的
}
}
void solve() {
cin >> n >> m;
e.assign(2 * n + 1, vector<int>());
for (int i = 1, x, y; i < n; i++) {
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
for (int i = 1; i <= n; i++) {
cin >> w[i];
dp[i].reset();
vis[i] = false;
}
dp[0].reset();
ans.reset();
dp[0][0] = 1; // 初始化特例
Work(1, n);
assert(m <= 100000);
for (int i = 1; i <= m; i++) cout << ans[i];
cout << "\n";
}
int main() {
ios_fast;
int T;
cin >> T;
while (T--) solve();
}
507. I. Incoming Asteroids【经典:三个点的权值之和大于x + 势能思想】【提交记录】
题意大致如下:有n个位置可以假设照相机,然后有m个时间发生。
- 事件1:有一个人在q[1..k]这k个位置各放了1个照相机,只有当照相机拍满y分钟的视频之后才会满足条件。满足条件之后就不架了。
- 事件2:在位置x出现了长达y分钟的事件,在位置x架设的照相机可以拍到y分钟的视频。
每个事件2之后输出满足条件的事件1的ID。要求强制在线。
如果不要求强制在线,可以离线用二分来做,也是。
如果一个事件1要拍y分钟,那么一定存在一个位置拍了分钟及以上的。我们把这个时间戳放进一个堆里面,当一个位置x加了y的时候,检查一下x节点的堆是否有事件1的时间戳被满足。
这样不停减直到变成0。所以整体复杂度为
508. 2019香港-E. Erasing Numbers【贪心 + 思维 + 结论】
题意:给你n=2000的一个序列(保证任意两个数互不相同),每次可以选择连续的3个数,然后保留其中的中位数,删掉剩下两个数。请你判断是否能通过一系列操作最后只剩第i个数。
重点:最重要的就是怎么实现贪心的过程了。
① 假设现在是比较多,即。现在应该让元素之间内耗,从而达到的数量和的数量相等。而内耗的唯一方法就是个连在一起,即的时候发生。此时发生内耗,减变成完成内耗。基于这个思想,我们进行下面的分类讨论:
- 遇到的是:
- 直接即可。
- 遇到的是:这个不是我们希望遇到的,因为这会导致我们无法消掉连续的
- 此时,说明左边的已经内耗完成,直接抛弃这个就行了。
- 此时,说明左边还有存在,可以等待和右边的内耗,所以可以消掉这个尽可能把传到右边去。
- 遇到的是:
- 肯定无法越过,所以直接赋值即可。
② 如果是比较多,同样分析就行。
查看代码
const int maxn = 5e3 + 1;
int n, a[maxn];
int test(int pos) {
int delta = 0, cnt = 0, tot = 0;
for (int i = 1; i <= n; i++)
if (i != pos) delta += a[i] < a[pos] ? -1 : 1;
for (int i = 1; i <= n; i++) {
if (pos == i) {
cnt = 0;
} else if (a[i] < a[pos]) {
cnt--;
if (delta < 0) {
if (cnt == -3) cnt += 2, tot++;
} else {
cnt = max(cnt, 0); // -1不是多的,如果不取max的话,后续会消耗1而导致tot++的机会变少,所以直接抛弃
}
} else if (a[i] > a[pos]) {
cnt++;
if (delta > 0) {
if (cnt == 3) cnt -= 2, tot++;
} else {
cnt = min(cnt, 0); // 1不要消耗-1,所以直接抛弃这个1
}
}
}
return 2 * tot >= abs(delta);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cout << test(i);
cout << "\n";
}
}
509. H. Harmonious Rectangle【抽屉原理】
题意:给定一个的网格,每个位置可以涂上颜色1、2、3这3种颜色。求有多少个合法的涂色方案使得存在四元组满足和,以及下面两个条件之一:
思路:
考虑一个二元组的种涂色方案,在的网格里,必然存在两行是满足上面条件的。然后dfs暴力搜出以内的所有不合法的解。对于或者的输入,直接输出就行了。
510. E. Identical Parity【exgcd + 思维】
思路:
令,那么有个块的长度是, 个块的长度是。
同时1的数量, 0的数量。
设把个大块设置为1,个小块设置为1,那么有:
同时还有:。
所以得到了这些限制条件:
① - 这个条件一开始check就行了
②
我们先求出方程 的特解, 那么有: 和
得到的两个合法范围区间: 和。最后检查是否成立就行了。
查看代码
long long n, k;
long long exgcd(long long a, long long b, long long &x, long long &y) {
if (!b) {
x = 1, y = 0;
return a;
} else {
long long g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
}
void solve() {
cin >> n >> k;
long long one = (n + 1) / 2;
long long even = n - one;
long long a_cnt = n % k, a = (n + k - 1) / k;
long long b_cnt = k - a_cnt, b = n / k;
if (n != a_cnt * a + b_cnt * b) { // 特判不合法情况
cout << "No\n";
return;
}
long long x, y, A = a, B = b, C = one, g = exgcd(A, B, x, y);
if (C % g || !A || !B) { // 特判无解
cout << "No\n";
return;
}
C /= g, A /= g, B /= g;
x *= C, y *= C; // 记得通解乘上C
long long Lx = (int)ceil((-1.0 * x) / (1.0 * B)), Rx = (int)floor((1.0 * a_cnt - x) / (1.0 * B));
long long Ry = (int)floor((-1.0 * y) / (-1.0 * A)), Ly = (int)ceil((1.0 * b_cnt - y) / (-1.0 * A)); // 乘一个负数,区间翻转
if (max(Lx, Ly) <= min(Rx, Ry)) {
cout << "Yes\n";
} else {
cout << "No\n";
}
}
511. F. Hossam and Range Minimum Query【主席树 + 随机集合哈希】
题意:1e5次询问,对于一个长度为1e5的序列,求区间内出现次数为奇数的最小值,强制在线。
看到强制在线,要么考虑分块,要么考虑主席树等等。
分块我一开始想了想,发现需要用到值域分块,而且值域分块合并不了信息,所以不能做。
考虑主席树(多个版本的权值线段树),区间内怎么判断是不是出现次数为奇数呢?
我们考虑去掉所有出现次数为偶数的数。即建立主席树的时候,遇到一个数是第"奇数次"出现的,那么就+rand_hash_value,否则就-rand_hash_value。也可以使用异或+nealhash。
最后用L-1的主席树和R的主席树去查询,如果左节点的hash值相同(这里涉及到pushup函数合并hash值,具体你自己设计就行,只要不发生hash冲突),那说明左侧没有出现过奇数次的数,否则就说明左侧出现过。(这个就是主席树上二分的过程)
查看代码
struct custom_hash {
static uint64_t splitmix64(uint64_t x) {
x += 0x9e3779b97f4a7c15;
x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9;
x = (x ^ (x >> 27)) * 0x94d049bb133111eb;
return x ^ (x >> 31);
}
size_t operator()(uint64_t x) const {
static const uint64_t FIXED_RANDOM =
chrono::steady_clock::now().time_since_epoch().count();
return splitmix64(x + FIXED_RANDOM);
}
} Hash;
int n, q, l, r, a[maxn], s[maxn], sc;
int top, rt[maxn], ls[maxn * 20], rs[maxn * 20];
uint64_t H[maxn * 20];
#define mseg ((l + r) >> 1)
int id(int x) { return lower_bound(s + 1, s + 1 + sc, x) - s; }
int insert(int p, int l, int r, int x) {
int ro = ++top;
ls[ro] = ls[p], rs[ro] = rs[p], H[ro] = Hash(s[x]) ^ H[p];
if (l == r) return ro;
if (x <= mseg) ls[ro] = insert(ls[p], l, mseg, x);
else rs[ro] = insert(rs[p], mseg + 1, r, x);
return ro;
}
int query(int a, int b, int l, int r) {
if (l > r) return 0;
if (l == r) {
return H[a] == H[b] ? 0 : s[l];
} else if (H[ls[a]] == H[ls[b]]) {
return query(rs[a], rs[b], mseg + 1, r);
} else {
return query(ls[a], ls[b], l, mseg);
}
}
signed main() {
ios::sync_with_stdio();
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], s[i] = a[i];
sort(s + 1, s + 1 + n);
sc = unique(s + 1, s + 1 + n) - s - 1;
for (int i = 1; i <= n; i++)
rt[i] = insert(rt[i - 1], 1, sc, id(a[i]));
cin >> q;
int lasans = 0;
while (q--) {
cin >> l >> r;
l ^= lasans;
r ^= lasans;
lasans = query(rt[l - 1], rt[r], 1, sc);
cout << lasans << "\n";
}
}
512. B - Arrange Your Balls【构造】
题意:定义cnt为(x,y)的对数,其中xy相邻且(x,y)。且现在给你一个环,让你重排一些位置,使得cnt最少。
我们把两种颜色相邻看成一条边,那么这显然是一个无向图。所以答案最小只能是k-1,其中k是颜色的数量。最小的情况下,颜色作为节点形成一棵树。也就是说,答案要么是k-1,要么是k。
我们现在判断能不能形成一棵树。首先把所有颜色的出现次数设置为min(count, k-1),因为只有k-1是有效的!
然后每次取出一个出现次数最小的节点,把他当成叶子节点,挂在另一个非叶子节点上。我们贪心地选择出现次数最大地节点为这个非叶子节点。如果两个都是叶子节点,这种情况只能出现在最后一刻,否则说明无解,答案只能是k。我们得到树形结构之后,使用欧拉序,相邻的颜色同样只有k-1对,就能构造出k-1的答案。很神奇。(性质:欧拉序上相邻的位置在树上也是相邻的)
对了,杜老师是基于一个性质提出这个构造方法的:颜色出现的次数必须大于等于节点的度数。
【扩展】给你一个度数序列,构造出一个简单图。
(和上面恰恰相反)每一次找度数最大的点,把它的度数分配到后面的点(从大往小排序)。直到度数不够后面的点分(比如:3 3 1 1)就说明答案无解,否则就构造出一个贪心的解。
513. E - GCD of Path Weights【图论 + GCD转换为取模 + 势能思想】【提交记录】
题意:给定一个有向无环图(因为u->v一定有u<v,所以无环),同时给你一个点权序列A,(a[i]=-1说明点权未确定)求1到n的所有路径权值(路径权值为点权之和)的gcd。
(1)总结套路:
杜老师说遇到这类全部路径的题,可以考虑定义一个函数代表1到所有路径的一个权值,至于这个权值的含义由题目解法决定。
但是这个题实际上先转换问题才能使用这个套路。
(2)考虑已经知道答案,假设答案为:
现在定义为从1到的所有路径的权值之和模得到的余数。那么有:
-
- 我们在后面的讨论中都不再考虑的情况,因为一个未知数导致该等式恒成立,也就是不会影响到的大小。这也就是为什么我们后面不对它连边。
这就是我们通过假设得到的全部信息。我们也考虑通过这些信息构造出一个最大的答案。
(3)如何根据这个模型贪心构造捏?
因为和成立,所以必然是和的一个因子。所以即对所有的边取一遍GCD。最后再。
不仅仅如此,我们有办法让取gcd的次数尽可能少。比如 的过程,加法对任意的模数都是成立的,也就是说,当是从推导过来的时候,是不影响答案的。所以我们应该建一个无向图(而不是原题的有向无环图),对于同一个连通块,我们应该尽可能地从1个点推导出所有地点(因为取gcd的次数肯定越少越好)。对于无向图的一个环,我们没有办法,只能通过gcd来合并这个答案了。
【Winter-Camp构造】
(Camp构造小专题)增量构造
- 定义?:根据n-1或者n-2的答案构造出n的答案,或者根据n的答案构造出n-1的答案。
514:E. Oscar is All You Need【构造 + 增量构造例题】
没写出来,太烦了不写了。
(Camp构造小专题)调增法
515:Graph Coloring
题意:给你一个n=1e5的无向简单图,让你对图进行三染色,同时保证每个节点的度数不超过5,要满足颜色和自身相同的点不能超过1个。输出最后的染色方案,保证有解。
题解:大概就是暴力地check和修改,每次修改完之后还要check邻边的点,这样并不能保证每个点只会被遍历一遍,但是为了避免一个点在对列中多次出现,还是要维护一个vis数组。总的复杂度比较玄学和随机,但是毕竟图限制得很死,所以应该是能过的。
查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 11;
int n, m, c[maxn], vis[maxn];
vector<vector<int>> e;
bool check(int x) {
int cnt = 0;
for (int v : e[x])
if (c[v] == c[x]) cnt++;
return cnt > 1;
}
int Work(int x) {
int cnt[3] = {0, 0, 0};
for (const int& v : e[x]) cnt[c[v]]++;
int mn = min({cnt[0], cnt[1], cnt[2]});
if (mn == cnt[0]) return c[x] = 0;
if (mn == cnt[1]) return c[x] = 1;
return c[x] = 2;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
double st = clock();
freopen("test_input.txt", "r", stdin);
freopen("test_output.txt", "w", stdout);
cerr << "start" << endl;
cin >> n >> m;
e.assign(n + 1, {});
size_t mx_deg = 0;
for (int i = 1, x, y; i <= m; i++) {
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
mx_deg = max(mx_deg, e[x].size());
mx_deg = max(mx_deg, e[y].size());
}
cerr << "input ok - max_deg = " << mx_deg << endl;
for (int i = 1; i <= n; i++) c[i] = i % 3;
queue<int> q;
for (int i = 1; i <= n; i++)
if (check(i)) q.push(i), vis[i] = 1;
while (q.size()) {
int x = q.front();
Work(x), q.pop(), vis[x] = 0;
for (const int& v : e[x])
if (!vis[v] && check(v)) q.push(v);
}
cerr << "solve ok" << endl;
for (int i = 1; i <= n; i++) cout << c[i] << " ";
cout << endl;
double et = clock();
cerr << "OK! " << (et - st) << "ms" << endl;
}
516:[ARC076F] Exhausted 【霍尔定理 + 二分图匹配 或者 反悔贪心】
(1)方法1:霍尔定理
霍尔定理:对于一个二分图,任意一个的子集都满足:是该二分图能完美匹配的充要条件。
推论-最大匹配。其中取遍的子集,为的相邻节点的集合。
一看题意,就好像能用二分图匹配来做。但是直接建图会TLE。我们知道答案就是。考虑到图的特殊性,使用霍尔定理来计算最大匹配。把人作为部点,那么。但是我们不可能真的枚举所有子集。所以我们枚举【使用扫描线的思想】,然后把所有的人都放入到某个数据结构中,再从这些人里面选一个集合出来,使得最大。使用线段树维护后缀最大值就可以做到。
(2)方法2:反悔贪心【题解】
我们把人按从小到大排序,然后正着贪心一遍,把人尽可能往左边坐。那么剩下了一些人,我们让这些人的尽可能小。具体怎么做呢?
一开始设置数组为全-1,然后如果坐了一个人就是0,否则就是证明有suf[i]个人的坐标要大于等于i。
对于当前枚举到第个人,左边已经坐到:
- : 这个人可以继续往左边坐。
- : 从所有的人中,找到一个最小的,让他尽可能往右边坐,即只能坐区间。
这么贪心到最后,我们再倒着贪心,做一遍后缀和就是贪心的结果。
517:Pairs of Pairs【无向图DFS树 + 构造思想】
呃,这个题的最重要的思想就是:
- 如果树的深度没有,那么说明每一层就算少一个,最后的节点数量一定至少有个。
- 我们按层来划分节点对,一定不会多余两条边:
- 因为同一层之间不存在边,那么只剩下返祖边和儿子边。
- 因为图不存在重边,所以要么是返祖边(返祖边返回的一定是祖先),要么是儿子边。
- 然后对于返祖边,一层只能有一个祖先,所以最多只能有2条边。如果是儿子和父亲之间的边,也最多只有2条。
至此,构造的正确性就全部说明了。
518:C. Johnny Solving【无向图DFS树 + 度数至少为3 + 构造思想】
如果DFS树的深度超过,那么就是任务1。
否则树的叶子节点一定超过个。 题目说节点度数大于等于3,所以构造一下就是答案了。
519:Hidden Supervisors 【树上贪心 + 排序细节】
结论:树上最大匹配可以贪心地做。
然后排序也很重要,应该先让空余节点多的子树排前面。
520:Problem H. 还原神作 【WQS二分优化DP】【WQS二分教程】
对于最小值,显然具有一个暴力DP的解法。我们可以考虑 wqs优化。
二分斜率M,感性理解就是:我们省略掉代价大于M的转移,那么二分出来的就是最低转移代价(也就是说,在这道题目中,斜率就是转移代价)。
注意这个重点!!!!
- DP转移应该先使得答案最优,再从最优答案里面,使得转移的段数K最大。而不是先让K最大,然后再让答案最优。(我因为这里wa3)
其实对于wqs二分的题目,我一直没有考虑过严格证明,也不会严格证明,只是通过感性理解觉得能够二分这个代价,然后套上模板。
下面是看完教程之后的一个感性的证明。
假设是时的答案,显然他是一个单调不减的函数,甚至是斜率单调不减(这个很重要,这意味着这个函数是一个下凸包)。因为你多选一对,这一对的代价一定不小于之前选择的,否则在之前选择会使得答案更小。(以下小写代表斜率,大写代表题目给定的要求)
然后我们考虑使用一条直线去切这个函数,固定一个斜率,直线切这个下凸包,得到的一定比其它更小(这和我们最小化答案的方向是一致的) - 这也解释了上面为什么要先最小化答案,再最大化段数,否则你就不是在求切点。我们先考虑求出在处的切线,即先求出,那么最终答案就是 - 这是因为直线的斜率是,切点位于x轴处,截距加上就是切点的纵坐标。
我们怎么求这个呢?因为斜率越大,切点就越往右走,所以我们考虑二分出这个斜率,然后可以看出,每一步被记录的转移,代价减少了。而我们最后求出的DP数组和计数数组,就是在斜率下,得到的截距和横坐标。
直到找到了切点横坐标的那条切线,我们就找到了答案。
521:G - Go Territory (atcoder.jp)
枚举X轴,然后用Y相邻的两个点围出来的线段当成一个点。然后从X=1到X=2e5枚举一遍,如果相邻的线段有重叠,那么两个点之间连一条无向边。这样连完之后,跑一边BFS,看看从0号线段能不能到达就行了。如果不能到达,就意味着被封了起来。对这些线段求和就好了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具