NOIP 训练做题笔记
CF191C
树上差分模板题。
CF165E
对每个 \(i\),随便求一个 \(j\),使得 $ a_i \& a_j = 0, i \neq j $。
注意到题目要求,以及值域较小,考虑 SOSDP,即求出 \(f(mask)\) 为任意两个下标 \(i, j\),满足 \(a_i \subseteq mask, a_j \subseteq mask\)。
对于每个 \(i\),找到 \(f(\sim a_i)\) 里的一个 \(\neq i\) 的下标就好。
AT_arc098_b
我们都知道,\(\text{xor}\) 是不进位的加法。
那么如果想要满足区间异或和和区间和相等,二进制上的加法必须没有进位。
注意到如果 \(seg_a \subset seg_b\),那么 \(seg_a\) 合法是 \(seg_b\) 合法的必要条件。
所以可以使用双指针。
P1950
问格子图上有多少个完全空白的子矩阵。
直接枚举矩阵的底边,然后就化作了在一个柱状图里找子矩阵。
我们可以进一步枚举底边一定包含的点,然后对于这个点的 \(h_i\),先用单调栈找到左边第一个 \(\le h_i\) 的下标 \(l\),右边第一个 \(< h_i\) 的下标 \(r\)。
然后这个区间 \([l, r]\) 内,矩阵可以取任意高度。并且我们发现,这样一定不会算重。其实一个很好的解释方法就是:让每个矩形被她的底边上最靠左的 \(\min_{l \le i \le r}(h_i)\) 计算一次。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 1007;
int n, m, nxt[N][N], h[N];
int nl[N], nr[N], stk[N], tp;
bool mat[N][N];
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
char ch; cin >> ch;
mat[i][j] = (ch == '.');
if(!mat[i][j]) nxt[i][j] = i;
else nxt[i][j] = nxt[i - 1][j];
}
}
int ans = 0;
for(int ed = 1; ed <= n; ed ++){
for(int i = 1; i <= m; i ++) h[i] = ed - nxt[ed][i];
h[0] = -1e9, h[m + 1] = -1, stk[tp = 1] = 0;
for(int i = 0; i <= m + 1; i ++){
while(tp && h[stk[tp]] > h[i]) nr[stk[tp --]] = i;
nl[i] = stk[tp], stk[++ tp] = i;
}
for(int i = 1; i <= m; i ++){
ans += (i - nl[i]) * (nr[i] - i) * h[i];
}
}
cout << ans << "\n";
return 0;
}
用相同的方法可以通过 AT_abc420_f,就是多了个面积限制,推式子就好了。
CF1407D
显然只需要处理其中一种转移即可,我们考虑 \(h_i, h_j > \max_{i < k < j}(h_k)\) 的情况。
显然维护一个递减的单调栈,每次将 \(i\) 插入的时候,将 \(i\) 从所有被弹出的下标转移一次就好了。
P2882
这题显然不满足单调性,而且数据范围较小。所以可以考虑枚举 \(K\) 。接下来只需要从前往后贪心就可以了,没有后效性。
P8865
这题是一个简单的预处理。
怎么算 C 呢?
首先预处理每个点往右,最多延伸几个格子 \(g(i, j)\)。
先考虑枚举竖的那条边的横坐标 \(j\)。然后看一下这一列上 \(g(*, j)\) 的两两乘积之和,可以用前缀和做到 \(\mathcal{O}(n)\)。
如果是 F 的话,在前缀和的时候,记录一下这个点往下可以延伸多少格,乘进去就好了。
P3017
首先二分。然后考虑如何分割最优。显然我们从左往右考虑分割:加入上一次分完的右端点是 \(last\),那么如何快速判断 \(last ~ i\) 这一段区间内是否可以分为 \(B\) 段,使得每一段不小于二分的答案?显然也可以大贪特贪。
P3625
很有意思,如何求三个不相交的子矩阵最大和。
首先,考虑一个一维上的问题。我想求一个数组的互不相交的两个 \(k\) 长子区间的最大权值和。
如果你有一点基本的算法素养,就会知道可以枚举断点,然后预处理前缀和后缀的最大和,时间复杂度可以从 \(\mathcal{O}(n^2) \to \mathcal{O}(n)\)。
考虑这种优化的本质是什么:你通过分析答案的拓扑关系,来把原本不确定的两个位置,化成了可以被预处理的,规范形状。
推广到这一题,不难发现只需要两种情况就可以覆盖:

对于第二种情况,直接压缩成一维解决。
对于第一种情况,可以先处理上半部分,再和下半部分合并。
当然你需要考虑所有旋转过后的情况,我这里用了一种很懒的实现方法。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 1507;
const int NEG = -1000000000;
int n, m, k;
signed a[N][N], pre[N][N], lu[N][N], ru[N][N], ld[N][N], rd[N][N], rb[N];
void rot(){
static signed tmp[N][N];
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
tmp[j][n - i + 1] = a[i][j];
}
}
swap(n, m);
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++) a[i][j] = tmp[i][j];
}
}
int solve(){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
pre[i][j] = a[i][j] + pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1];
}
}
for(int i = 0; i <= n + 1; i ++){
for(int j = 0; j <= m + 1; j ++){
lu[i][j] = ru[i][j] = ld[i][j] = rd[i][j] = NEG;
}
}
for(int i = 1; i <= n; i ++) rb[i] = NEG;
for(int i = k; i <= n; i ++){
for(int j = k; j <= m; j ++){
int s = pre[i][j] - pre[i - k][j] - pre[i][j - k] + pre[i - k][j - k];
if(s > lu[i][j]) lu[i][j] = s;
if(s > ru[i][j - k + 1]) ru[i][j - k + 1] = s;
if(s > ld[i - k + 1][j]) ld[i - k + 1][j] = s;
if(s > rd[i - k + 1][j - k + 1]) rd[i - k + 1][j - k + 1] = s;
if(s > rb[i]) rb[i] = s;
}
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
lu[i][j] = max(lu[i][j], max(lu[i - 1][j], lu[i][j - 1]));
for(int i = 1; i <= n; i ++)
for(int j = m; j >= 1; j --)
ru[i][j] = max(ru[i][j], max(ru[i - 1][j], ru[i][j + 1]));
for(int i = n; i >= 1; i --)
for(int j = 1; j <= m; j ++)
ld[i][j] = max(ld[i][j], max(ld[i + 1][j], ld[i][j - 1]));
for(int i = n; i >= 1; i --)
for(int j = m; j >= 1; j --)
rd[i][j] = max(rd[i][j], max(rd[i + 1][j], rd[i][j + 1]));
int res = 0;
for(int i = k; i + k <= n; i ++){
for(int j = k; j + k <= m; j ++){
int v = (int)lu[i][m] + ld[i + 1][j] + rd[i + 1][j + 1];
if(v > res) res = v;
}
}
for(int a = k + 1; a + 2 * k <= n + 1; a ++){
int v = (int)lu[a - 1][m] + rb[a + k - 1] + ld[a + k][m];
if(v > res) res = v;
}
return res;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++) cin >> a[i][j];
}
int ans = 0;
for(int i = 0; i < 4; i ++){
rot(), ans = max(ans, solve());
}
cout << ans << "\n";
return 0;
}
P2513
插入 DP + 前缀和。
CF2025D
这种题很符合 DP 的特征。
\(f(i, j)\) 代表目前在第 \(i\) 为,一个属性为 \(j\) 的最大通过数(第二个属性的记录是没有必要的)。
如果 \(r_{i + 1} = 0\),那么我可以选择 \(f(i, j) \to f(i + 1, j)\),或者 \(f(i, j) \to f(i, j + 1)\)。
如果是对一种属性,可以直接通过区间 \(+1\) 的方式搞定。
用个滚动数组。区间 \(+1\),直接差分解决,然后如果遇到 \(r = 0\),那就暴力还原数组。
AT_abc311_f
这是一个数数题,比较有 DP 的特征,具体分析如下:
想象一下这个选取的过程,其实就是找几列 \(j\) 上选择一个点 \((*, j)\),然后直接转移就好了。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 2007;
int f[N][N], n, m, mn[N];
char ch[N][N];
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++){
cin >> ch[i][j];
}
}
f[0][n + 1] = 1;
for(int i = 1; i <= m; i ++){
mn[i] = n + 1;
for(int j = 1; j <= n; j ++){
if(ch[j][i] == '#'){mn[i] = j; break;}
}
int sum = 0;
for(int j = n + 1; j >= 1; j --){
sum += f[i - 1][j], sum %= 998244353;
(f[i][j] = sum + f[i - 1][j - 1]) %= 998244353;
if(j > mn[i]) f[i][j] = 0;
}
}
int ans = 0;
for(int i = 1; i <= n + 1; i ++)
(ans += f[m][i]) %= 998244353;
cout << ans << "\n";
return 0;
}
P6869
两点的路径是唯一的。用树上差分求出每条边经过的次数,分别考虑每条边买单程还是多程就好了。
其实还有一个很有意思的视角:考虑一个点连向她父亲的边被遍历的次数,就是这个子树里的最大连续字段的个数。这个可以用 DSU on tree 解决。虽然复杂度并不占优,但是是一种本质的方法。
CF739B
首先有一个非常无脑的做法,就是直接统计子树。
考虑 \(u \in \text{anc}(v)\),那么 \(u\) 控制 \(v\) 当且仅当 \(dep_u \ge dep_v - a_v\),DSU on tree 即可解决。具体地,开一个权值线段树存储 RHS,然后查询前缀和。
但是有一个简单的方法,就是考虑 \(v\) 对于她所有祖先的贡献。观察一下可以发现一定是一段从 \(v\) 开始向上的区间。直接二分 + 树上差分就做完了。
给出法 1 的代码,能过。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 2e5 + 7, M = 10000007, LIM = 200000000000000;
int n, a[N], dep[N], wei[N], sz[N], son[N], ans[N];
vector<int> g[N];
signed ls[M], rs[M], cnt[M];
signed tot, rt;
int newnode(){
++ tot, ls[tot] = rs[tot] = cnt[tot] = 0;
return tot;
}
void add(signed &u, int l, int r, int x, int v){
if(!u) u = newnode();
cnt[u] += v;
if(l == r) return;
int mid = l + (r - l) / 2;
if(x <= mid) add(ls[u], l, mid, x, v);
else add(rs[u], mid + 1, r, x, v);
}
void ins(int x){ add(rt, 0, LIM, x, 1); }
int leq(int u, int l, int r, int x){
if(!u || x < l) return 0;
if(r <= x) return cnt[u];
int mid = l + (r - l) / 2;
return leq(ls[u], l, mid, x) + leq(rs[u], mid + 1, r, x);
}
int qry(int x){ return x < 0 ? 0 : leq(rt, 0, LIM, x); }
void clr(){ tot = 0, rt = newnode(); }
void cal(int u){
sz[u] = 1;
for(int v: g[u]){
dep[v] = dep[u] + wei[v], cal(v), sz[u] += sz[v];
if(sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs2(int u){
ins(dep[u] - a[u] > 0 ? dep[u] - a[u] : 0);
for(int v: g[u]) dfs2(v);
}
void dfs(int u){
for(int v: g[u]){
if(v != son[u]) dfs(v), clr();
}
if(son[u]) dfs(son[u]);
for(int v: g[u]){
if(v != son[u]) dfs2(v);
}
ins(dep[u] - a[u] > 0 ? dep[u] - a[u] : 0);
ans[u] = qry(dep[u]);
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
for(int i = 2; i <= n; i ++){
int p, w; cin >> p >> w;
wei[i] = w, g[p].push_back(i);
}
rt = newnode();
cal(1);
dfs(1);
for(int i = 1; i <= n; i ++) cout << ans[i] - 1 << " ";
cout << "\n";
return 0;
}
P1600
这是一个典题了。我们不妨把一条链拆成 \(u \to \text{lca}(u, v)\) 和 \(\text{lca}(u, v) \to v\) 两部分。直接考虑左边部分。
\(x\) 想要被贡献,必须要满足 \(x\) 在 \(u \to \text{lca}(u, v)\) 上,并且 \(w_x + dep_x = dep_u\)。
显然,就是看每一个点,被和她权值相同的链覆盖了几次。
首先直接考虑对每一个点开一个桶,然后树上差分解决。
但是由于实际上差分影响的点只有 \(\mathcal{O}(n)\) 个。所以可以直接考虑对每个点打标记。
直接在链的下端打标记,然后 DFS,首次碰到某个点的时候,记录一下目前的 \(cnt_{col_u}\),遍历完子树后,将目前的 \(cnt_{col_u}\) 和进入子树时的做差即可。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 3e5 + 7, OFF = N;
int n, m, w[N], dep[N], fa[N], x[N], y[N], lc[N], ans[N];
vector<int> g[N], t1a[N], t1b[N], t2a[N], t2b[N];
int c1[2 * N], c2[4 * N];
int top[N], sz[N], son[N];
void cal(int u, int pre){
sz[u] = 1;
dep[u] = dep[pre] + 1;
fa[u] = pre;
for(int v: g[u]) if(v != pre){
cal(v, u), sz[u] += sz[v];
if(sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs(int u, int pre, int cur){
top[u] = cur;
for(int v: g[u]) if(v != pre && v != son[u]) dfs(v, u, v);
if(son[u]) dfs(son[u], u, cur);
}
int lca(int u, int v){
while(top[u] != top[v]){
if(dep[top[u]] >= dep[top[v]]) u = fa[top[u]];
else v = fa[top[v]];
}
if(dep[u] <= dep[v]) return u;
return v;
}
void getans(int u, int pre){
int p1 = c1[dep[u] + w[u]], p2 = c2[dep[u] - w[u] + OFF];
for(int t: t1a[u]) c1[t] ++;
for(int t: t1b[u]) c1[t] --;
for(int t: t2a[u]) c2[t + OFF] ++;
for(int t: t2b[u]) c2[t + OFF] --;
for(int v: g[u]) if(v != pre) getans(v, u);
ans[u] = (c1[dep[u] + w[u]] - p1) + (c2[dep[u] - w[u] + OFF] - p2);
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for(int i = 1; i < n; i ++){
int u, v; cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for(int i = 1; i <= n; i ++) cin >> w[i];
cal(1, 0), dfs(1, 0, 1);
for(int i = 1; i <= m; i ++){
cin >> x[i] >> y[i];
lc[i] = lca(x[i], y[i]);
t1a[x[i]].push_back(dep[x[i]]);
t1b[fa[lc[i]]].push_back(dep[x[i]]);
t2a[y[i]].push_back(2 * dep[lc[i]] - dep[x[i]]);
t2b[lc[i]].push_back(2 * dep[lc[i]] - dep[x[i]]);
}
getans(1, 0);
for(int i = 1; i <= n; i ++) cout << ans[i] << " ";
cout << "\n";
return 0;
}
P4211
请务必只维护有需要的东西,这是绝对不劣的原则。
首先,题目要求回答 \(q\) 次询问,求
直接统计不方便,转化成统计每个点在这个区间内当了几次 \(\text{lca}\)。
但是发现刚好卡在这个点上似乎不是很好算。但是同时发现对每个点算出来的计算次数还需要 \(\times dep\),索性答案转化成
此时发现,\(cnt_{\text{lca} \in \text{subtree}(u)}\) 其实就是 \(i, z\) 都在 \(u\) 子树里的 \(i\) 个数。
于是可以想到莫队:添加/删除一个点 \(u\),就是将 \(u\) 到根的路径上的点 \(\pm 1\),将目前的区间对于 \(z\) 查询,就是查询 \(z\) 到根的权值和。
但是这个是 \(\mathcal{O}(n \sqrt{n} \log^2 n)\) 的。如果不是 \(\text{poly}\) 还能过。
看看这个式子,感觉能优化的只有莫队了。并且发现对于这种可以差分的答案,使用莫队维护真的是一件很蠢的事情。
首先不难想到将点的编号看成时间轴,对每个点 \(u\) 分别维护
但是时空均是 \(\mathcal{O}(n^2)\) 起步,太不牛了。但是维护这么多东西真的是我需要的吗?我想要的只是每次询问的答案,也就是说,所有点需要维护的 \([1, i]\) 的 \(i\) 个数加起来其实是 \(\mathcal{O}(n)\) 级别的。
拆成两个询问,离线到线段树上,做完了。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 5e4 + 7, MOD = 201314;
int top[N], fa[N], dep[N], sz[N], son[N], id[N], idx;
vector<int> g[N]; int n, m;
struct SGT{
int val[N << 2], tag[N << 2];
#define ls (x << 1)
#define rs ((x << 1) | 1)
#define mid ((l + r) >> 1)
void add(int x, int l, int r, int ql, int qr, int k){
val[x] += (min(qr, r) - max(ql, l) + 1) * k;
if(ql <= l && r <= qr) return tag[x] += k, void();
if(ql <= mid) add(ls, l, mid, ql, qr, k);
if(mid < qr) add(rs, mid + 1, r, ql, qr, k);
}
int qur(int x, int l, int r, int ql, int qr){
if(ql <= l && r <= qr) return val[x];
int res = tag[x] * (min(qr, r) - max(ql, l) + 1);
if(ql <= mid) res += qur(ls, l, mid, ql, qr);
if(mid < qr) res += qur(rs, mid + 1, r, ql, qr);
return res;
}
} sgt;
void cal(int u){
dep[u] = dep[fa[u]] + 1;
sz[u] = 1;
for(int v: g[u]){
fa[v] = u, cal(v);
sz[u] += sz[v];
if(sz[v] > sz[son[u]]) son[u] = v;
}
}
void dfs(int u, int cur){
top[u] = cur; id[u] = ++ idx;
if(son[u]) dfs(son[u], cur);
for(int v: g[u]) if(v != son[u]) dfs(v, v);
}
int opr(int x, int k){
int res = 0;
while(x){
sgt.add(1, 1, n, id[top[x]], id[x], k);
res += sgt.qur(1, 1, n, id[top[x]], id[x]);
x = fa[top[x]];
}
return res;
}
int ans[N];
struct Qur{
int e, id, k, z;
} s[N << 1];
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m;
for(int i = 1; i < n; i ++){
int xx; cin >> xx;
g[xx + 1].push_back(i + 1);
}
cal(1);
dfs(1, 1);
for(int i = 1; i <= m; i ++){
int l, r, z; cin >> l >> r >> z;
l ++, r ++, z ++;
s[i].e = r, s[i + m].e = l - 1;
s[i].id = s[i + m].id = i;
s[i].k = 1, s[i + m].k = -1;
s[i].z = s[i + m].z = z;
}
sort(s + 1, s + (m << 1) + 1, [](Qur a, Qur b){
return a.e < b.e;
});
for(int t = 1, i = 0; i <= n; i ++){
if(i) opr(i, 1);
while(t <= (m << 1) && s[t].e <= i){
ans[s[t].id] += s[t].k * opr(s[t].z, 0);
t ++;
}
}
for(int i = 1; i <= m; i ++) cout << (ans[i] % MOD + MOD) % MOD << "\n";
return 0;
}
AT_arc100_c
因为需要对每个 \(K\) 求,所以转化成「对每个 \(K\) 求 $i\ \text{or}\ j \subseteq K $」肯定不劣,而且更好算。
因为 \(\subseteq\) 可以使用 SOSDP 来维护:具体地,对每个 \(K\) 求出 \(i \subseteq K\) 的最大的两个 \(A_i\),取前缀最大值即可。
AT_arc136_d
首先考虑某一个元素有多少个可以匹配的元素。然后直接 SOSDP 即可。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 1e6 + 7;
int n, a[N], cnt[N];
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> a[i];
cnt[a[i]] ++;
}
//999999
for(int pos = 0, pw = 1; pos < 6; pos ++, pw *= 10){
for(int i = 0; i <= 999999; i ++){
int dig = i / pw % 10;
if(dig) cnt[i] += cnt[i - pw];
}
}
int ans = 0, slf = 0;
for(int i = 1; i <= n; i ++){
int cur = a[i], res = 0;
bool ok = 1;
for(int j = 0, pw = 1; j < 6; j ++, pw *= 10){
int dig = cur / pw % 10;
res += pw * (9 - dig);
if(dig > 4) ok = 0;
}
ans += cnt[res];
slf += ok;
}
cout << (ans - slf) / 2 << "\n";
return 0;
}
CF449D
与和为 \(0\),很难搞,而且没法直接组合计数。
是不是可以松弛一下条件:我们去算与和 \(s\) 的个数,满足 \(K \subseteq s\)。
这样就能组合计数了,\(f_s = 2^{cnt_s}\),\(cnt_s\) 代表 \(s \subseteq a_i\) 的 \(a_i\) 个数。
并且注意到值域很小,所以可以想到 SOSDP。求 \(cnt_s\) 可以 SOSDP,还原 \(f_s\) 也可以 SOSDP。
CF1234F
首先这个操作,本质上就是允许你把没有重复字符的字符串拆成两段,求最大长度和。
然后看到这个字符集其实很有说法,\(20\) 而不是 \(26\),启示我们这题可能可以用状压表示字符串内的字符集。
然后最长字符串长度不超过 \(20\),所以可以考虑枚举一个字符串,然后找另外一个。
试完善程序,我们存储 \(f_i, g_i\),代表字符集为 \(i\) 的子集的子串的最小 \(r\) 和最大 \(l\),枚举子串,再到 SOSDP 数组里找即可。
CF1208F
终于知道为啥没写代码了,简单题。
SOSDP 有一个非常牛的地方在于,她可以把与和 \(i\) 存在松弛成 \(i\) 的超集个数 \(\ge 2\),并且根据最优性原理,一定可以覆盖最优解。
既然是找三元组,先把 \((j, k)\) 找出来。令 \(f_{mask}\) 代表 \(mask \subseteq a_j\ \&\ a_k\) 的最大 \(j\)。然后对于每个 \(a_i\),我们直接按位贪心取即可。
其实还有一种方法,就是把问题转化成 \((\sim a_i)\ \&\ (a_j\ \&\ a_k)\) 的最大值,这样和刚刚是一样的。

随机做题
浙公网安备 33010602011771号