202411 题目合集(2)
P10217
- 无论是最优化问题还是计数问题,如果没有想法->考虑判定。
- 答案能否为 \(m\)?
- 这个 \((x, y)\) 很容易让人想到向量。同时 \(i\bmod n\) 是一个周期一样的东西。记给定的 \(n\) 个分别是向量 \(\boldsymbol{v_0, v_2,\dots, v_{n - 1}}\),\(\boldsymbol{z_j} = \sum\limits_{i = 0}^{j}\boldsymbol{v_i}\)。那么对于 \(x\) 仅仅考虑这些的位移就是 \(\lfloor\dfrac{m - 1}{n}\rfloor z_{n -1} + z_{(m - 1) \bmod n}\)。要求能够通过加上一个 \(\boldsymbol{w}\) 使得 \(\lfloor\dfrac{m - 1}{n}\rfloor \boldsymbol{z_{n - 1}} + \boldsymbol{z_{(m - 1)\bmod n}} + \boldsymbol{w} = \boldsymbol{a}\)。其中 \(\boldsymbol{a}\) 为给定的向量。所以 \(\boldsymbol{w}\) 是可以根据 \(m\) 唯一确定的。考虑这个 \(\boldsymbol{w}\) 能否通过 \(m\) 次,每次横纵上的位移绝对值之和不超过 \(k\)。也就是说对于 \(\boldsymbol{w} = (x, y)\) 只需要 \(|x| + |y| \le k\times m\) 即可。我们得到了 \(O(1)\) 的判定方法。
- 考虑优化。上面的式子根据 \((m - 1)\bmod n\) 的剩余系分类。设 \(r = (m - 1) \bmod n, t = \lfloor \dfrac{m - 1}{n}\rfloor\)。问题变成对于每一个 \(r\) 求出最小的 \(t\) 使得 \(\boldsymbol{a} - \boldsymbol{z_r} - t\boldsymbol{z_{n - 1}} = \boldsymbol{w}\) 中 \(|x_w| + |y_w| \le km\)。以下的 \(x, y\) 均是合成的向量的坐标,不是读入的值。\(|x_w| = |x_a - x_r - tx_{n - 1}|, |y_w| = |y_a - y_r - ty_{n - 1}|, |x_w| + |y_w| = |x_a - x_r - tx_{n - 1}| + |y_a - y_r - ty_{n - 1}| \le km = k(nt+r)\)
- 真是讨厌的绝对值!但是由于这里绝对值就是这个式子可能的最大的,所以可以直接暴力去掉绝对值,列出关于 \(t\) 的四个不等式,求出 \(t\) check(方法见上)即可。
#include <bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
const int N = 1e6;
struct line {
ll x, y;
} Q[N + 10];
il ll Abs(ll x) {
return ((x > 0) ? (x) : (-x));
}
int n;
ll k, edx, edy;
il bool check(ll p) {
if(!p) {
if(!edx && !edy) return 1;
else return 0;
}
if(p < 0) return 0;
ll wz = (p - 1) / n, wp = (p - 1) % n;
line tmp = (line){Q[n - 1].x * wz + Q[wp].x, Q[n - 1].y * wz + Q[wp].y};
// cout << tmp.x << ' ' << tmp.y << endl;
ll dist = Abs(tmp.x - edx) + Abs(tmp.y - edy);
return (dist <= p * k);
}
il ll ceildiv(ll n, ll m) {
if(n * m < 0) {
n = Abs(n), m = Abs(m);
return -(n / m);
}
else {
n = Abs(n), m = Abs(m);
return n / m + ((n % m == 0) ? (0) : (1));
}
}
il ll floordiv(ll n, ll m) {
if(n * m < 0) {
n = Abs(n), m = Abs(m);
return -ceildiv(n, m);
}
else {
n = Abs(n), m = Abs(m);
return n / m;
}
}
vector <ll> vec;
const int D[2] = {1, -1};
int cnt = 0;
il ll calc(int p) {
ll xz = Q[n - 1].x, yz = Q[n - 1].y;
ll xr = edx - Q[p].x, yr = edy - Q[p].y;
vec.clear();
for(int a = 0; a < 2; a++) {
for(int b = 0; b < 2; b++) {
ll faca = D[a] * xz + D[b] * yz - n * k;
ll facb = k * (p + 1) + D[a] * xr + D[b] * yr;
if(!faca) continue;
vec.push_back(Abs(ceildiv(facb, faca)) * n + p + 1);
}
}
sort(vec.begin(), vec.end());
for(int i = 0; i < vec.size(); i++)
if(check(vec[i])) return vec[i];
return -1;
}
void init() {
cin >> n >> k >> edx >> edy;
cin >> Q[0].x >> Q[0].y;
for(int i = 1; i < n; i++) {
ll xx, yy; cin >> xx >> yy;
Q[i] = (line){Q[i - 1].x + xx, Q[i - 1].y + yy};
}
if(!edx && !edy) {
cout << 0 << endl;
return ;
}
ll minn = -1;
//// cout << calc(0) << endl;
// cout << check(399999999) << endl;
// cout << check(399999998) << endl;
for(int i = 0; i < n; i++) {
// cout << i << endl;
ll rest = calc(i);
if(rest == -1) continue;
else {
if(minn == -1) minn = rest;
else minn = min(minn, rest);
}
}
cout << minn << '\n';
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while(T--) init();
}
总结
- 可以利用向量来刻画 \(x, y\) 分别相加!
- 对于绝对值和的小于等于,可以把它暴力拆成 \(2^k\) 个不带绝对值的式子。
P1502
发现不会扫描线典题耶( •̀ ω •́ ),糖丸了
- 直接考虑似乎不太行,于是考虑每一颗星星对答案的贡献。
- 先特别考虑一下这个“在边框上不算在内”的条件。把最下面一行的星星放到框上,那么要求最上面一行的 \(y \le y_{low} +h - 1\)。横向的
- 我们可以利用右上角唯一确定一个矩形,同时也可以据此将每颗星星可能出现在的矩形的范围划分出来:对于星星 \(x, y\),能够圈住它的右上角在 \((x, y)\) 到 \((x + w - 1, y + h - 1)\) 构成的矩形内。
- 我们拿这个右上角代表整个矩形,那么问题变成了:求出一个重叠贡献最大的区域。
- 这个时候就可以扫描线了!令 \(y\) 上有一条 \([x, x + w)\) 的线,边权为 \(+c\),\(y+h - 1\) 上有一条 \([x, x + w)\) 的线,边权为 \(-c\)。做普普通通的扫描线即可。
- 这道题的线段树维护的元素有两种形式:一种是维护 \([x_1, x_2)[x_2, x_3)[x_3, x_4)\) 一样的线段的权值。在这种情况下所离散化的点就是 \((x+w)\),每次的编号也都要 \(-1\)。而另一种是单独维护各个点:\(x_1, x_2, x_3, x_4\),所以这个时候离散化下的点就是 \((x + w - 1)\),线段树编号不需要 \(-1\)。这是一个需要注意的细节!我小时候应该是抄题解写的,非常不厚道。
#include <bits/stdc++.h>
#define il inline
using namespace std;
const int N = 1e5;
struct seg {
int l, r, x, val;
} Q[N * 2 + 10];
int cmax[N * 4 + 10], tag[N * 4 + 10];
il int ls(int x) {
return 2 * x;
}
il int rs(int x) {
return 2 * x + 1;
}
il void push_up(int x) {
cmax[x] = max(cmax[ls(x)], cmax[rs(x)]);
}
il void push_tag(int now, int val) {
cmax[now] += val;
tag[now] += val;
}
il void push_down(int now) {
if(tag[now] != 0) {
push_tag(ls(now), tag[now]);
push_tag(rs(now), tag[now]);
tag[now] = 0;
}
}
void upd(int ql, int qr, int s, int t, int now, int val) {
if(ql <= s && t <= qr) {
push_tag(now, val);
return ;
}
int mid = (s + t) >> 1;
push_down(now);
if(ql <= mid) upd(ql, qr, s, mid, ls(now), val);
if(qr > mid) upd(ql, qr, mid + 1, t, rs(now), val);
push_up(now);
}
int qry(int ql, int qr, int s, int t, int now) {
if(ql <= s && t <= qr) return cmax[now];
int mid = (s + t) >> 1, maxn = 0;
push_down(now);
if(ql <= mid) maxn = max(maxn, qry(ql, qr, s, mid, ls(now)));
if(qr > mid) maxn = max(maxn, qry(ql, qr, mid + 1, t, rs(now)));
return maxn;
}
bool cmp(seg &x, seg &y) {
if(x.x == y.x) return x.val > y.val;
else return x.x < y.x;
}
int pos[N * 2 + 10];
int n, w, h;
void init() {
memset(cmax, 0, sizeof(cmax));
memset(tag, 0, sizeof(tag));
cin >> n >> w >> h;
for(int i = 1, x, y, val; i <= n; i++) {
cin >> x >> y >> val;
Q[2 * i - 1] = (seg){x, x + w, y, val};
Q[2 * i] = (seg){x, x + w, y + h - 1, -val};
pos[2 * i - 1] = x, pos[2 * i] = x + w;
}
sort(Q + 1, Q + 2 * n + 1, cmp);
sort(pos + 1, pos + 2 * n + 1);
int ans = 0;
int len = unique(pos + 1, pos + 2 * n + 1) - pos - 1;
for(int i = 1; i <= 2 * n; i++) {
Q[i].l = lower_bound(pos + 1, pos + len + 1, Q[i].l) - pos;
Q[i].r = lower_bound(pos + 1, pos + len + 1, Q[i].r) - pos;
upd(Q[i].l, Q[i].r - 1, 1, len, 1, Q[i].val);
ans = max(ans, cmax[1]);
}
cout << ans << endl;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while(T--) init();
}
P9190
这题没做出来还是太唐诗了吧 TAT
暴力 dp:\(f[i, j]\) 代表能否让前 \(i\) 个文本串匹配到模式串第 \(j\) 位(模式串看成是 bessie 反复重合,因为它没有 border,这样是可行的),发现 \(j\) 只有最后对 6 取余不同的 6 个才有用,所以设 \(f[i, j](j \in [0, 6])\) 代表目前匹配到 bessie 的第 \(j\) 位出现了多少 bessie,再带上一个最小删除权值即可。因为它是满足最优子结构性质的。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5, inf = 2e8;
struct node {
int len, val;
} f[N + 10][10];
int a[N + 10], n;
string str;
string b = "#bessie";
int main() {
cin >> str; str = "#" + str;
n = str.size() - 1;
for(int i = 1; i <= n; i++)
cin >> a[i];
f[0][0] = (node){0, 0};
for(int i = 1; i <= 6; i++)
f[0][i] = (node){-inf, inf};
for(int i = 1; i <= n; i++) {
f[i][0] = (node){f[i - 1][0].len, f[i - 1][0].val};
for(int j = 1; j <= 6; j++) {
f[i][j] = (node){f[i - 1][j].len, f[i - 1][j].val + a[i]};
if(str[i] == b[j]) {
if(f[i - 1][j - 1].len < f[i][j].len) continue;
else if(f[i - 1][j - 1].len == f[i][j].len) f[i][j].val = min(f[i][j].val, f[i - 1][j - 1].val);
else f[i][j] = (node){f[i - 1][j - 1].len, f[i - 1][j - 1].val};
}
}
if(f[i][6].len + 1 > f[i][0].len)
f[i][0] = (node){f[i][6].len + 1, f[i][6].val};
else if(f[i][6].len + 1 == f[i][0].len)
f[i][0].val = min(f[i][6].val, f[i][0].val);
// for(int j = 0; j <= 6; j++)
// cout << f[i][j].len << ' ' << f[i][j].val << endl;
//
// cout << endl;
}
cout << f[n][0].len << '\n' << f[n][0].val << '\n';
}
总结
- 要及时删除不必要的状态。显然,这题中大部分 \(j\) 都是无用的,只有最后 7 个 \(j\) 才有用。
- 对于 dp 如果满足最优子结构性质,不妨设一个二元组。这其实和 dp 本身的设计逻辑不相悖,即便他不常见。
P8272
模拟赛被创死,展现出普及组选手 cfm 强大的普及水准。
什么时候 \(i\) 可以接住苹果 \(a\)?(\(t_i \ge t_a\))
- 当 \(x_i \ge x_a\) 时,有 \(t_i - t_a \ge x_i - x_a\)。\(t_i - x_i \ge t_a - x_a\)。
- 当 \(x_i < x_a\) 时,有 \(t_i - t_a \ge x_a - x_i\)。\(t_i + x_i \ge t_a + x_a\)。
赛时的时候列出了后面两个式子,但是仍然并不会这道题。
我们注意到后面两个式子一定同时成立!这一点在移向前的式子非常明显,因为 \(t_i - t_ a\ge |x_i - x_a|\)。但是移向后似乎就没有这么显然了(upd:其实也非常显然,上下两式相加得到 \(t_i \ge t_a\)……,反过来只要减一下就好了……)
因此令 \(t_i - x_i = x_i, t_i + x_i = y_i\),那么 \(i\) 可以接住苹果 \(a\) 的充要条件就是 \(x_i \ge x_a, y_i \ge y_a\)。(这里的 \(x\) 和上面的 \(x\) 不是一样的!)这是一个二维偏序问题,但一般的二维偏序是计数,这里是最优化:将数对匹配。
回顾二维偏序的过程,是先对 \(x\) 进行排序,然后利用数据结构维护 \(y\)。这里我们也按照 \(x\) 排序以此消除 \(x_i \ge x_a\) 的约束。对于每个奶牛 \(i\) 考虑把能吃掉的苹果放到集合 \(S\) 中。从 \(x\) 小的奶牛往 \(x\) 大的奶牛考虑。不考虑删除时这个 \(S\) 是在增长的。接下来考虑删除,显然 \(y\) 越小的苹果越难被匹配。所以从 \(y\) 尽量小的奶牛开始匹配。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5;
struct node {
int opt, x, y, num;
bool operator < (const node &other) const {
return y < other.y;
}
} Q[N + 10];
bool cmp(node &x, node &y) {
return x.x > y.x;
}
multiset <node> S;
int n;
vector <node> vec[3];
int main() {
cin >> n;
for(int i = 1, t, x; i <= n; i++) {
cin >> Q[i].opt >> t >> x >> Q[i].num;
Q[i].x = t - x, Q[i].y = t + x;
vec[Q[i].opt].push_back(Q[i]);
}
sort(vec[1].begin(), vec[1].end(), cmp);
sort(vec[2].begin(), vec[2].end(), cmp);
int ia = 0, sum = 0;
for(int i = 0; i < vec[1].size(); i++) {
while(ia < vec[2].size() &&
vec[2][ia].x >= vec[1][i].x) {
S.insert(vec[2][ia]);
ia++;
}
while(1) {
if(!vec[1][i].num) break;
set <node>::iterator it = S.lower_bound(vec[1][i]);
if(it == S.end()) break;
node tmp = (*it);
if(tmp.num >= vec[1][i].num) {
sum += vec[1][i].num;
tmp.num -= vec[1][i].num;
vec[1][i].num = 0;
S.erase(it);
S.insert(tmp);
break;
}
else {
sum += tmp.num;
vec[1][i].num -= tmp.num;
S.erase(it);
}
}
}
cout << sum << endl;
}
总结
真的是一个很值得反思反思的题目。
复盘过程
- 在得到充要条件的一步,因为一般来讲是要根据 \(x\) 的大小讨论一下的。因此少前置约束是一个值得的动机。
- 从另一个角度,以少前置约束出发就是 \(t_i - t_a \ge |x_i - x_a|\),等价于 \(t_i - t_a \ge x_i - x_a, t_i - t_a \ge x_a - x_i\) 同时成立。即 \(x \ge |y|\) 等价于 \(x \ge y, x \ge -y\)。这个变形我之前似乎没有用过,记下来
- 于是得到了一个类二维偏序的匹配问题。对于一个高维问题,最常用的思想是降维。三位偏序二维偏序如此,高维前缀和如此,高维统计也如此。
- 高维问题
- 降维
- 独立维度
- 高维问题
- 接下来的贪心中,是从“难以匹配”的奶牛和苹果出发。这也启发对于匹配类的贪心问题应该从“难以匹配”的元素出发思考策略。
- 这是因为一般来说匹配问题不带权(比如这道题万一苹果带权似乎就不好贪心了)。难以匹配的赶紧匹配,容易匹配的放到后面匹配也没关系。这是“决策包容性”。
- 关于这块似乎还要结合几个线段贪心详细总结总结
P10161
-
考虑对一个串有多少合法子串进行计数。
-
不妨考虑增量(类似与一个区间 dp 的思想,划分子问题)
- 如果往外套一层 (),那么合法子串数量 +1
- 如果往外面加一个 (),令目前这一层有 \(k\) 个 ()()()(),那么合法子串数量 +k
-
可以想到这样求出能够构造 \(i\) 个合法子串的最少序列长度 \(f_i\):每次先构造出
()()()()的,然后给外面套层(()()()()),接下来同理。转移只要考虑套一层 (),外面增加 \(j-1\) 个(),一共会增加 \((j + 1)j/2\) 个。dp 即可。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5;
ll nd[N + 10], rt[N + 10];
int getn(int x) {
int l = 1, r = 1000, ans = 0;
while(l <= r) {
int mid = (l + r) >> 1;
if(mid * (mid + 1) / 2 <= x) ans = mid, l = mid + 1;
else r = mid - 1;
}
return ans;
}
void prep() {
for(int i = 1; i <= N; i++) {
int w = getn(i);
nd[i] = N * 10;
for(int j = 1; j <= w; j++) {
int ww = (j + 1) * j / 2;
if(nd[i - ww] + 2 * j < nd[i]) {
rt[i] = j;
nd[i] = nd[i - ww] + 2 * j;
}
}
}
}
void print(int x) {
if(x == 1) {
putchar('(');
putchar(')');
return ;
}
if(x == 0)
return ;
int w = rt[x];
putchar('(');
print(x - (w + 1) * w / 2);
putchar(')');
for(int i = 1; i < w; i++)
putchar('('), putchar(')');
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
prep();
int T; cin >> T;
while(T--) {
int n, qk;
cin >> n >> qk;
if(n >= nd[qk]) {
print(qk);
for(int i = nd[qk] + 1; i <= n; i++)
putchar('(');
puts("");
}
else puts("-1");
}
}
总结
- 这道题我一开始想贪心去了……确实不应该,我怎么知道这道题能不能贪?
- 不要把题目想太复杂,大胆一些。
- 感觉这题很萌?
P3226
\(x\) 和 \(2x, 3x\) 的约束关系形成一颗树,问题变成树上最大独立集——错啦!比如 \(1->2, 1->3, 2->6, 3->6\)——是 DAG 上最大独立集。我会网络流!
这道题 n = 1e5 网络流不了,怎么办,考虑图的性质。发现 1->2->6,1->3->6 构成一个很有意思的矩形,实际上如果把 1->2->4->...->x->2x,3->6->...->x->2x 之类列出来,可以得到一个矩形,问题变成了矩形上的最大独立集。列数最多 \(O(\log_3 n)\),可以状压。由于互相间没有相邻 \(1\) 的数量并不大,所以跑得动。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 16, M = 11, V = 1e5;
const ll Mod = 1e9 + 1;
ll f[N + 3][(1 << M) + 3];
int num[N + 10], n, tot = 0;
bool flag[V + 10];
vector <int> sc;
void prepsc() {
for(int S = 0; S < (1 << 16); S++) {
bool flag = 0;
for(int i = 0; i < 15; i++)
if(((S >> i) & 1) && ((S >> (i + 1)) & 1)) {
flag = 1;
break;
}
if(flag) continue;
else sc.push_back(S);
}
}
ll calc() {
f[0][0] = 1;
for(int i = 1; i < tot; i++) {
for(int s1 = 0; s1 < sc.size(); s1++)
f[i][s1] = 0;
for(int s0 = 0; s0 < sc.size(); s0++) {
if(sc[s0] > (1 << num[i - 1]) - 1) break;
for(int s1 = 0; s1 < sc.size(); s1++) {
if(sc[s1] > (1 << num[i]) - 1) break;
int S0 = sc[s0], S1 = sc[s1];
if((S0 & S1) > 0) continue;
f[i][s1] = (f[i][s1] + f[i - 1][s0]) % Mod;
}
}
}
ll sum = 0;
for(int S = 0; S < sc.size(); S++)
sum = (sum + f[tot - 1][S]) % Mod;
return sum;
}
void work() {
ll ans = 1;
for(int i = 1; i <= n; i++) {
if(!flag[i]) {
tot = 1;
for(int j = i; j <= n; j *= 2) {
if(flag[j]) break;
num[tot] = 0;
for(int k = j; k <= n; k *= 3) {
flag[k] = 1;
num[tot]++;
}
tot++;
}
ans = ans * calc() % Mod;
}
}
cout << ans << endl;
}
int main() {
cin >> n;
prepsc();
work();
}
总结
- 用图刻画“能否同时出现”的信息。因为这类信息都具有一定传递性(?)或者相互影响。
- 无法继续往下做怎么办?——分析性质!这道题的性质在于每个点的出边都是 \(2\),最多入边也都是 \(2\),所以可以类似的考虑一个矩形上的独立集计数。
P2157
状态设 \(f[i, S, k]\) 为 \((i - 1)\) 的人都吃过了,接下来 \(i \sim i + 7\) 人内有 \(S\) 的人吃了,最后一个吃饭的人是 \((i + k)\) 的最短等待时间。
转移并不难
- 如果 \(i\) 没有吃,那么就在 \(S\) 内选择 \(x\not\in S\) 作为最后一个吃饭的,转移到 \(f[i, S + \{x\}, x - i]\)。要特别判断插队的不能互相间有影响。
- 如果 \(i\) 吃了(即 \(S\) 中包含 \(i\)),那么转移到 \(f[i + 1, S - \{i\}, k - 1]\)。
如果设出来了状态,转移相当平凡,但是怎么样设计出这个状态?
首先考虑:有很多排列的方案。于是可以从增量的角度考量一个队伍:最后一个放谁?
- 需要考虑一个极长存在的前缀 \(1\sim x\),同时 \((x + 1)\) 不存在。这是因为 \(1\sim x\) 前缀内放的一定合法,其它的都是插了 \((x + 1)\) 的队。插了队的也互相间没有影响,所以关键就在 \((x + 1)\) 合不合法。
- 这就启发我们将这个 \(x\) 作为阶段。
- 接下来一切都顺其自然:因为我们需要知道队伍的全集,它可以用 \(x, S\) 来刻画。同时我们需要知道最后一个人计算代价,它可以用 \(k\) 刻画。
#include <bits/stdc++.h>
using namespace std;
const int N = 1000, B = 8;
const int inf = 1e9;
int t[N + 10], b[N + 10], n;
int f[N + 3][(1 << B) + 3][B * 2 + 3];
void upd(int &x, int y) {
x = min(x, y);
}
void init() {
cin >> n;
for(int i = 1; i <= n; i++)
cin >> t[i] >> b[i];
memset(f, 0x3f, sizeof(f));
for(int i = 0; i <= b[1]; i++)
f[1][(1 << i)][i + B] = 0;
for(int i = 1; i <= n; i++) {
for(int S = 0; S < (1 << B); S++) {
for(int P = 0; P <= 2 * B; P++) {
int p = i + P - B;
if(p <= 0) continue;
for(int x = i; x <= i + b[i]; x++) {
bool flag = 0;
for(int j = (x - i); j <= 7; j++)
if((S >> j) & 1)
if(j + i - x > b[x]) {//以防选 S 内插队的出现不合法
flag = 1;
break;
}
if(flag) continue;
if(!((S >> (x - i)) & 1)) {
upd(f[i][S | (1 << (x - i))][x - i + B],
f[i][S][P] + (t[x] | t[p]) - (t[x] & t[p]));
}
}
if((S & 1) && P > 0) upd(f[i + 1][S >> 1][P - 1], f[i][S][P]);
}
}
}
// cout << f[1][2][1 + B] << endl;
// cout << f[1][10][3 + B] << endl;
// cout << f[1][14][2 + B] << endl;
// cout << f[1][46][5 + B] << endl;
// cout << f[1][62][4 + B] << endl;
// cout << f[1][190][7 + B] << endl;
// cout << f[1][191][B] << endl;
int minn = inf;
for(int i = max(n - 7, 1); i <= n; i++) {
for(int P = 0; P <= 2 * B; P++) {
int S = (1 << (n - i + 1)) - 1;
// if(f[i][S][P] == 9) {
// cout << i << ' ' << S << ' ' << P << endl;
// cout << f[i][S][P] << endl;
// }
minn = min(minn, f[i][S][P]);
}
}
cout << minn << endl;
}
int main() {
int T; cin >> T;
while(T--) init();
}
总结
- 无论是计数问题还是最优化问题,在没有思路的时候都应该考虑判定。
- 如果考虑判定没有什么启发(这道题就没有),可以尝试增量(这道题就是通过对”增加最后一个元素“考量分析的)
- 我 noip 炸了,这是 noip 后第一篇总结。真的不能再摆烂了,我与 cfm 共勉。
P4107
- 对于菊花图
- 显然是从 \(son(v) + c(v)-1\) 尽量小的点向父亲合并。
- 对于链的合并。
- 贪心的从链底向上合并。
- 为什么是正确的。
- 对于菊花图实际上就是按照 \(c(v)\) 大小排序。
- 反证法,假设不是正确的,即从链底 \(t\) 开始一直向上最多可以合并到 \(f\),但是从另外一个点 \(s\) 开始最多可以合并到更高的 \(h\)。那么从 \(s\) 开始首先会至少失去 \(t\) 到 \(s\) 这一段的受益,其次从 \(f\) 在网上可以继续合并到 \(h\)。失去的收益(没有删除的点)至少为 \(1\),而增加的收益(删除了的点)最多为 \(1\)(也就是 \(f\) 可以被删除),因此是不优的,可以直接贪。
- 对于全局的合并,就是直接按照深度从大往小 \(son(v) + c(v) - 1\) 大小向父亲合并即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6;
vector <int> gra[N + 10];
int dep[N + 10], fat[N + 10], son[N + 10];
int n, m, c[N + 10];
void dfs(int u) {
dep[u] = dep[fat[u]] + 1;
for(int i = 0; i < gra[u].size(); i++) {
int v = gra[u][i];
dfs(v);
}
}
struct node {
int to, cval;
bool operator < (const node &other) const {
return cval > other.cval;
}
};
int ord[N + 10];
bool cmp(int &x, int &y) {
return dep[x] > dep[y];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> c[i];
for(int i = 1; i <= n; i++) {
int x; cin >> son[i];
ord[i] = i;
for(int j = 1; j <= son[i]; j++) {
cin >> x;
fat[x + 1] = i;
gra[i].push_back(x + 1);
}
}
dfs(1);
int cnt = 0, last = 1;
sort(ord + 1, ord + n + 1, cmp);
priority_queue <node> que;
for(int i = dep[ord[1]]; i >= 2; i--) {
while(dep[ord[last]] == i && last <= n)
que.push((node){ord[last], c[ord[last]] + son[ord[last]] - 1}), last++;
while(!que.empty()) {
node fr = que.top(); que.pop();
int u = fr.to, cv = fr.cval;
int f = fat[u];
if(son[f] + c[f] + cv <= m) {
c[f] += c[u];
son[f] = son[f] - 1 + son[u];
cnt++;
}
}
}
cout << cnt << endl;
}
总结
- 对于树上最优化问题,如果没办法 dp(这东西看上去就不像 dp)可以考虑贪心
- 对于树上贪心策略
- 先从菊花图考虑,再从链考虑。
- 整体的框架很多时候都是从拓扑序开始。
- 菊花图的情况是为了得到策略,而链的情况是为了说明拓扑序更新的正确性。
- 正所谓:贪心向爹合并(X
P3594
答案显然具有单调性。于是二分。考虑判定。
如何判定一个区间最后可以不可以合法?只需要在其中取得长度为 \(d\) 和最大的一个子区间即可。这个单调队列就好了。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e6;
ll a[N + 10], S[N + 10], p;
int n, d;
int fr = 1, tl = 0;
int que[N + 10];
bool check(int x) {
int i = 1;
fr = 1, tl = 0;
for(int l = 1; l + x - 1 <= n; l++) {
while(fr <= tl && que[fr] < l) fr++;
while(i <= l + x - d) {
while(fr <= tl && S[que[tl] + d - 1] - S[que[tl] - 1] <=
S[i + d - 1] - S[i - 1]) tl--;
que[++tl] = i;
i++;
}
int u = que[fr];
if((S[l + x - 1] - S[l - 1]) - (S[que[fr] + d - 1] - S[que[fr] - 1]) <= p)
return 1;
}
return 0;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> p >> d;
for(int i = 1; i <= n; i++)
cin >> a[i], S[i] = S[i - 1] + a[i];
int L = d + 1, R = n, ans = d;
while(L <= R) {
int mid = (L + R) >> 1;
if(check(mid)) ans = mid, L = mid + 1;
else R = mid - 1;
}
cout << ans << endl;
}
P1954
什么?CSP2023T4?(大雾
这道题的策略也是类似的:按照拓扑序从 \(t\) 最短的出发。证明

浙公网安备 33010602011771号