NOIP 训练做题笔记 2
P1955
扩展域并查集 + 离散化。
P1381
哈希?+ 双指针。
CF190D
固定右端点 + 双指针。
P2564
这真不是黄?
P3594
首先单调性是肯定满足的,然后想办法维护双指针中间的最大子段和:
显然是删满 \(d\) 长最优,所以只需要维护 \(\mathcal{O}(n)\) 个和就好了。
P2216
单调队列套单调队列。
CF1913D
询问可以不断通过保留区间最小值的操作,从 \(p\) 到达的数组个数。
首先考虑怎样的一个排列,是可以从 \(p\) 达到的:不难发现,两次先后操作的区间要么互不相交,要么是包含关系。如果是包含关系的话,那么被包含的那个区间操作其实是无效的。因此,有效的操作肯定是将一堆互不相交的区间进行坍缩,求可以达成的不同数组个数。
为了不重不漏地计算数组个数,我们将数组放回 \(p\) 上考虑:即选出 \(p\) 的一个非空子序列。每个元素占据一个区间,然后必须是占据区间内的最小值。
那么此时可以考虑 DP。\(f_i\) 代表目前子序列最后一个元素在 \(i\) 的总和法方案数是多少,用单调栈预处理每个元素左右第一个更大的元素可以达到 \(\mathcal{O}(n)\) 转移。
既然都提到单调栈了,是不是可以优化转移过程?\(f_j \to f_i\) 的条件是,\(\min_{i < k < j} a_k \ge \min(a_i, a_j)\)。
所以,找到 \(a_i\) 左边第一个更小的 \(a_l\),那么 \(f_{(l, i)}\) 都可以进行转移。
接着,只能从 \(a_l\) 左边第一个更小的 \({a_l}_l\),\({a_l}_l\) 左边第一个更小的点转移,以此类推。
维护前缀和即可。
其实还有一种分治做法。
为了不重不漏地计数,还有一个突破口:考虑对于 \([1, n]\),最小值是必然存在的。
其实,相当一部分最值分治本质上就是在笛卡尔树上进行 DP,比如本题。
考虑将笛卡尔树建出来,问题就变成了在树上选择一个满足要求的集合。
考虑 \(f_u\) 代表 \(u\) 这个区间的取法数量。显然有:
当然,如果 \(u \neq root\),那么 \(u\) 有不选的情况。假如 \(u\) 是 \(fa_u\) 的右儿子,那么 \(u\) 以及 \(u\) 的左子树全部都不会选。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 3e5 + 7, MOD = 998244353;
int n, stk[N], tp, p[N], a[N], b[N], c[N], ord[N];
int ls[N], rs[N], rt;
void solve(){
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> p[i];
ls[i] = rs[i] = 0;
}
tp = 0;
for(int i = 1; i <= n; i ++){
int k = tp;
while(k && p[stk[k]] > p[i]) k --;
if(k) rs[stk[k]] = i;
if(k < tp) ls[i] = stk[k + 1];
tp = k, stk[++ tp] = i;
}
rt = stk[1];
for(int i = 1; i <= n; i ++) ord[i] = i;
sort(ord + 1, ord + n + 1, [](int x, int y){ return p[x] > p[y]; });
a[0] = b[0] = c[0] = 1;
for(int i = 1; i <= n; i ++){
int u = ord[i], lc = ls[u], rc = rs[u];
a[u] = a[lc] * (c[rc] + 1) % MOD;
b[u] = b[rc] * (c[lc] + 1) % MOD;
c[u] = ((c[lc] + 1) * (c[rc] + 1) % MOD - 2 + MOD) % MOD;
}
cout << a[ls[rt]] * b[rs[rt]] % MOD << "\n";
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
int t; cin >> t;
while(t --) solve();
return 0;
}
AT_abc234_g
首先这题不可能拆开算,所以直接笛卡尔树可能很麻烦。自然的方法还是考虑 DP。
令 \(f_i\) 代表将前 \(i\) 个分成若干段的贡献和,\(\max(l, r), \min(l, r)\) 分别标记 \([l, r]\) 的最大,最小值。
突然发现推到这一步可以拆开转移(而不是拆开 DP)。我们思考转移
使用前缀和优化即可。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 3e5 + 7, MOD = 998244353;
int n, a[N], f[N], g[N], h[N], sum[N];
int smx[N], tmx, smn[N], tmn;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
f[0] = 1;
sum[0] = 0, sum[1] = 1;
for(int i = 1; i <= n; i ++){
while(tmx && a[smx[tmx]] <= a[i]) tmx --;
int pre = smx[tmx]; smx[++ tmx] = i;
while(tmn && a[smn[tmn]] >= a[i]) tmn --;
int prn = smn[tmn]; smn[++ tmn] = i;
int dx = (sum[i] - sum[pre] + MOD) % MOD;
int dn = (sum[i] - sum[prn] + MOD) % MOD;
g[i] = (g[pre] + a[i] % MOD * dx) % MOD;
h[i] = (h[prn] + a[i] % MOD * dn) % MOD;
f[i] = (g[i] - h[i] + MOD) % MOD;
sum[i + 1] = (sum[i] + f[i]) % MOD;
}
cout << f[n] << "\n";
return 0;
}
P16902
这题的思路其实很明显,如果出现了一个值,满足 \(b_i = (r - i + 1)i\),那么 \(a_i\) 一定是 \(\max a\)。
以这个点为分割点向下分治。但是可能有多个分割点。所以可能遇到 \(b_i > (R - i + 1)(i - L + 1)\)。
此时,这个点,一定是上一层的最大值。
每次随便找一个 \(b_i > (R - i + 1)(i - L + 1)\) 的点向下递归即可。可以选择 \(\mathcal{O}(n \log n)\) 的“启发式分裂”分治或 \(\mathcal{O}(n)\) 的笛卡尔树做法。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 5e6 + 7;
int n, b[N], a[N];
void solve(int l, int r, int cnt){
if(l > r) return;
int L = l, R = r;
while(L <= R){
int x = (L - l + 1) * (r - L + 1);
if(b[L] == x){
a[L] = cnt;
solve(l, L - 1, cnt + 1), solve(L + 1, r, cnt + 1);
return;
}
else if(b[L] > x){
a[L] = cnt - 1;
solve(l, L - 1, cnt), solve(L + 1, r, cnt);
return;
}
x = (R - l + 1) * (r - R + 1);
if(b[R] == x){
a[R] = cnt;
solve(l, R - 1, cnt + 1), solve(R + 1, r, cnt + 1);
return;
}
if(b[R] > x){
a[R] = cnt - 1;
solve(l, R - 1, cnt), solve(R + 1, r, cnt);
return;
}
L ++, R --;
}
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++) cin >> b[i];
solve(1, n, 1);
for(int i = 1; i <= n; i ++) cout << a[i] << ' ';
return 0;
}
CF1730E
求可以最小值可以整除最大值的区间。
首先我们可以钦定其中一个,然后找另外一个。思索一下发现钦定最大值比较好做,为什么?
钦定最大值后,她的因数最多有 \(\mathcal{O}(\sqrt{n})\) 个,并且可以直接找出。
对于 \(i\),\(a_i\) 作为最大值有一个支配区间 \([l, r]\),满足 \(l \le i \le r\)。
考虑 \(a_i\) 的一个因数 \(x\),显然需要把 \([l, r]\) 种的所有 \(x\) 挑出来,可以 \(\mathcal{O}(n)\) 计算。当然这样还是 \(\mathcal{O}(n^{2.5})\),接下来就是纯推式子的事了。
当然还有一个我没想到的很牛方法:如果正着做不好做,还是一样:差分可以 \(0\) 代价将「刚好」松弛成「至少」。
这样,我们尝试解决,在 \([l, r]\) 内,包含 \(i\) 并且所有值 \(\ge x\) 的区间有多少个。
我们可以找到 \(i\) 向左,向右第一个 \(< x\) 的数,然后直接算非常简单。
CF526F
在一个 \(n \times n\) 的棋盘上摆放 \(n\) 个无法攻击的车等价于一个排列。
这样,题目本质上就是在询问 \(\max - \min + 1 = len\) 的区间个数。
前面我们做过一道同时涉及到 \(\max\) 和 \(\min\) 的 DP 题,这里我们同样尝试「右端点」式做法。
这样,枚举到右端点 \(r\) 时,\([1, r]\) 的一个后缀 \([l, r]\) 满足条件,当且仅当 \(r - l - \max + \min = -1\)。
考虑维护 \([1, r]\) 的每一个后缀的 \(\max\) 和 \(\min\)。如果在 \([1, r]\) 后追加 \(r + 1\),线段树 + 单调栈可以将每个后缀的最值维护转化为区间加问题。
我们既然变成区间加了,就可以使用线段树维护 \(r - l - \max + \min\) 了。只需要维护区间最小值以及最小值个数即可。
CF997E
双倍经验。
P4755
一眼直接上笛卡尔树,特征太明显了。但是分治是 \(\mathcal{O}(n^2)\) 的啊?
考虑到有一个叫“启发式分裂”的技巧,是否可以每层只遍历较短的那边,使得优化掉一个 \(n\) 呢?
有一种方法就是,遍历较短的区间内的所有元素,然后对于每个元素,二分另一段区间内的元素即可。但是二分就要排序,排序的最优做法就是归并,非常难受。
直接大力地上个主席树,或者归并树(树套 vector)查询区间 \(\le k\) 的值的个数,时间复杂度 \(\mathcal{O}(n \log^2 n)\)。
归并树做法其实已经足够优了,没什么浪费,但是其实考虑到所有查询都是固定的,离线下来放 BIT 上也可以。
#include <bits/stdc++.h>
#define int long long
#define Misaka namespace
#define Network std
using Misaka Network;
const int N = 1e5 + 7;
int n, a[N], stk[N], ls[N], rs[N], tp, rt, s1[N];
vector<int> t[N << 2];
void build(int x, int l, int r){
if(l == r) return t[x].push_back(a[l]), void();
int mid = (l + r) >> 1;
build(x << 1, l, mid), build(x << 1 | 1, mid + 1, r);
t[x].resize(r - l + 1);
merge(t[x << 1].begin(), t[x << 1].end(), t[x << 1 | 1].begin(), t[x << 1 | 1].end(), t[x].begin());
}
int qur(int x, int l, int r, int ql, int qr, int k){
if(ql > qr) return 0;
if(ql <= l && r <= qr) return upper_bound(t[x].begin(), t[x].end(), k) - t[x].begin();
int mid = (l + r) >> 1, res = 0;
if(ql <= mid) res += qur(x << 1, l, mid, ql, qr, k);
if(mid < qr) res += qur(x << 1 | 1, mid + 1, r, ql, qr, k);
return res;
}
int dfs(int l, int r, int rt){
int res = s1[r] - s1[l - 1];
if(ls[rt]) res += dfs(l, rt - 1, ls[rt]);
if(rs[rt]) res += dfs(rt + 1, r, rs[rt]);
int st = l, ed = rt - 1, pst = rt + 1, ped = r;
if(rt - l > r - rt) swap(st, pst), swap(ed, ped);
for(int i = st; i <= ed; i ++) res += qur(1, 1, n, pst, ped, a[rt] / a[i]);
return res;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> a[i];
s1[i] = s1[i - 1] + (a[i] == 1);
int k = tp;
while(k && a[i] > a[stk[k]]) k --;
if(k < tp) ls[i] = stk[k + 1];
if(k) rs[stk[k]] = i;
tp = k, stk[++ tp] = i;
}
rt = stk[1];
build(1, 1, n);
cout << dfs(1, n, rt) << "\n";
return 0;
}

浙公网安备 33010602011771号