莫队专题
普通莫队
SP3267 (模板)
普通莫队模板题,静态多次查询区间内所有数字出现的种类数。
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define fr first
#define se second
#define endl '\n'
#define pb push_back
//#define int long long
typedef long long ll;
typedef __int128 LL;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,int> pli;
const int maxn = 30005;
const int maxv = 1000005;
const int maxb = 170;
int n, q;
int a[maxn];
int cnt[maxv], kind=0;
int bi[maxn], bl[maxb], br[maxb];
struct Queries{
int l, r, id;
}qry[200005];
int ans[200005];
// 普通莫队的排序方式:先按左端点所在块号升序排序,再按右端点数值大小升序排序。
bool cmp(Queries a, Queries b){
if(bi[a.l] == bi[b.l]){
if(bi[a.l] & 1){
return a.r < b.r;
}
else{
return a.r > b.r;
}
}
else{
return bi[a.l] < bi[b.l];
}
}
void del(int x){
if(--cnt[x] == 0){
kind --;
}
}
void add(int x){
if(++cnt[x] == 1){
kind ++;
}
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
const int blen = sqrt(n);
const int bcnt = (n + blen - 1) / blen;
for(int i = 1; i <= n; i ++){
bi[i] = (i - 1) / blen + 1;
}
for(int i = 1; i <= bcnt; i ++){
bl[i] = (i - 1) * blen + 1;
br[i] = min(n, i * blen);
}
cin >> q;
for(int i = 1; i <= q; i ++){
int l, r;
cin >> l >> r;
qry[i] = {l, r, i};
}
sort(qry + 1, qry + q + 1, cmp);
int winl = 1, winr = 0; // 初始时滑动窗口内为空
for(auto &[l, r, id] : qry){
while(winl > l){
add(a[--winl]);
}
while(winr < r){
add(a[++winr]);
}
while(winl < l){
del(a[winl++]);
}
while(winr > r){
del(a[winr--]);
}
ans[id] = kind;
}
for(int i = 1; i <= q; i ++){
cout << ans[i] << "\n";
}
}
signed main()
{
// freopen("in.txt", "rt", stdin);
// freopen("out.txt", "wt", stdout);
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
// int T=1; cin>>T; while(T--)
solve();
return 0;
}
P3709
莫队解决 多查询区间 \([l,r]\) 内众数的出现次数。
由于每次修改对 cnt 的变化量只会是 +1,-1,因此区间众数出现次数是很容易动态维护的。
P4462
多查询区间 \([l,r]\) 中的异或和等于 \(k\)(常数,每次查询都一样)的子数组个数。
莫队维护 \(a\) 的前缀异或和 \(pre\) 即可。注意对于查询 \([l \backsim r]\) 需要维护的是 \(pre[l-1 \backsim r]\)。具体细节见 code。
带修莫队
对于每条查询,除了区间左右端点 \([l, r]\) 外,还需要维护一个关于修改的时间点 \(t\),表示在当前查询之前一共进行了多少次修改操作(执行的也就是在原查询序列中的前 \(t\) 条修改)。
同时我们也需要额外维护一个关于修改时间点的窗口,该窗口只有一个指针,前缀所有修改已经执行,后缀所有修改均未执行。对于每一条查询,我们需要前进 / 回退到对于该查询的修改时间点上,并对原序列和当前区间内的词频统计做相应修改 / 恢复。
P1903 (模板)
在多次查询区间 \([l,r]\) 内所有数字出现的种类数 的基础上,添加单点修改操作。
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define fr first
#define se second
#define endl '\n'
#define pb push_back
//#define int long long
typedef long long ll;
typedef __int128 LL;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,int> pli;
const int maxn = 133336;
const int maxv = 1000005;
const int maxb = 400;
int n, q, tot_q;
int a[maxn];
int cnt[maxv], kind=0;
int bi[maxn], bl[maxb], br[maxb];
struct Queries{
int l, r, t, id;
}qry[maxn];
int ans[maxn];
int upd_tim=0;
pii upd_seq[maxn];
// 带修莫队的排序方式:先按左端点所在块号升序排序,再按右端点所在块号升序排序,最后按照修改时间点升序排序。
bool cmp(Queries a, Queries b){
if(bi[a.l] == bi[b.l] && bi[a.r] == bi[b.r]){
return a.t < b.t;
}
if(bi[a.l] == bi[b.l]){
return bi[a.r] < bi[b.r];
}
return bi[a.l] < bi[b.l];
}
void del(int x){
if(--cnt[x] == 0){
kind --;
}
}
void add(int x){
if(++cnt[x] == 1){
kind ++;
}
}
// 修改时间点移动函数
void moveTime(int ql, int qr, int tim){
auto [pos, val] = upd_seq[tim];
if(ql <= pos && pos <= qr){
del(a[pos]);
add(val);
}
swap(upd_seq[tim].second, a[pos]);
}
void solve()
{
cin >> n >> q;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
const int blen = max(1, (int)pow(n, 2.0/3)); // 注意带修莫队的块长不再是根号,而是 2/3!!!
const int bcnt = (n + blen - 1) / blen;
for(int i = 1; i <= n; i ++){
bi[i] = (i - 1) / blen + 1;
}
for(int i = 1; i <= bcnt; i ++){
bl[i] = (i - 1) * blen + 1;
br[i] = min(n, i * blen);
}
for(int i = 1; i <= q; i ++){
char op; cin >> op;
if(op == 'Q'){
int l, r;
cin >> l >> r;
qry[++tot_q] = {l, r, upd_tim, tot_q};
}
else{
int pos, v;
cin >> pos >> v;
upd_seq[++upd_tim] = {pos, v};
}
}
sort(qry + 1, qry + tot_q + 1, cmp);
int winl = 1, winr = 0, wint = 0; // 初始时两个滑动窗口内均为空
for(auto &[l, r, t, id] : qry){
while(winl > l){
add(a[--winl]);
}
while(winr < r){
add(a[++winr]);
}
while(winl < l){
del(a[winl++]);
}
while(winr > r){
del(a[winr--]);
}
while(wint < t){
moveTime(l, r, ++wint);
}
while(wint > t){
moveTime(l, r, wint--);
}
ans[id] = kind;
}
for(int i = 1; i <= tot_q; i ++){
cout << ans[i] << "\n";
}
}
signed main()
{
// freopen("in.txt", "rt", stdin);
// freopen("out.txt", "wt", stdout);
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
// int T=1; cin>>T; while(T--)
solve();
return 0;
}
CF940F
Q:带有单点修改,多次查询给定区间 \([l, r]\),求对于当前序列的该子区间内 \(cnt_{0 \backsim 10^{9}}\) 的 mex,其中 \(cnt_{v}\) 表示 \(v\) 在 \([l,r]\) 内的出现次数。
类似上面 P3709,维护一个 \(cntt\) 数组,表示 \(cnt\) 的桶。用带修莫队实时维护每个查询区间的 \(cnt\) 和 \(cntt\)。
显然 \(mex\) 至少为 \(1\);而答案不会超过 \(O(\sqrt n)\)(很显然的根号性质),因此对于每个询问只需要通过暴力遍历 \(cntt\) 的方式找到答案。具体实现见 code。
回滚莫队
分成 只增回滚莫队 与 只删回滚莫队 两种。
只增回滚莫队
P14420 (模板)
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define fr first
#define se second
#define endl '\n'
#define pb push_back
//#define int long long
typedef long long ll;
typedef __int128 LL;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,int> pli;
const int maxn = 100005;
const int maxv = 100005;
const int maxb = 370;
int n, q, M;
int a[maxn], ori[maxn];
int cnt[maxv];
int bi[maxn], bl[maxb], br[maxb];
ll curAns=0;
ll backup;
struct Queries{
int l, r, id;
}qry[100005];
ll ans[100005];
// 只增回滚莫队的排序方式:先按左端点所在块号升序排序,再按右端点数值大小升序排序。(与普通莫队相同)
bool cmp(Queries a, Queries b){
if(bi[a.l] == bi[b.l]){
return a.r < b.r;
}
return bi[a.l] < bi[b.l];
}
// 由于是只增回滚莫队,当窗口缩减时不更新答案,只在窗口扩大时更新答案
void del(int x){
cnt[x] --;
}
void add(int x){
cnt[x] ++;
curAns = max(curAns, 1ll * ori[x] * cnt[x]);
}
// 暴力解决某个块内的区间的答案
ll bruteforce(int l, int r){
ll res = 0;
for(int i = l; i <= r; i ++){
cnt[a[i]] ++;
res = max(res, 1ll * ori[a[i]] * cnt[a[i]]);
}
for(int i = l; i <= r; i ++){
cnt[a[i]] --;
}
return res;
}
void solve()
{
cin >> n >> q;
vector<int> alls;
for(int i = 1; i <= n; i ++){
cin >> a[i];
alls.pb(a[i]);
}
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
M = alls.size();
map<int,int> mp;
for(int i = 0; i < M; i ++){
mp[alls[i]] = i + 1;
ori[i + 1] = alls[i];
}
for(int i = 1; i <= n; i ++) a[i] = mp[a[i]];
const int blen = sqrt(n);
const int bcnt = (n + blen - 1) / blen;
for(int i = 1; i <= n; i ++){
bi[i] = (i - 1) / blen + 1;
}
for(int i = 1; i <= bcnt; i ++){
bl[i] = (i - 1) * blen + 1;
br[i] = min(n, i * blen);
}
for(int i = 1; i <= q; i ++){
int l, r;
cin >> l >> r;
qry[i] = {l, r, i};
}
sort(qry + 1, qry + q + 1, cmp);
// 按照块的顺序遍历,依次解决每个查询
for(int block = 1, qi = 1; block <= bcnt && qi <= q; block ++){
curAns = 0;
for(int i = 1; i <= M; i ++) cnt[i] = 0;
int winl = br[block] + 1, winr = br[block]; // 初始化滑动窗口
while(qi <= q && bi[qry[qi].l] == block){ // 每一轮解决当前块内的所有查询
auto [l, r, id] = qry[qi];
if(r <= br[block]){ // 区间在整个块内,直接暴力
ans[id] = bruteforce(l, r);
}
else{
while(winr < r){ // 先扩窗口右端点
add(a[++winr]);
}
// !!当右端点扩展完成时,备份下来此时的答案,等到左端点扩展完后回滚时恢复 curAns
backup = curAns; // 备份,用于回滚
while(winl > l){ // 再扩窗口左端点
add(a[--winl]);
}
ans[id] = curAns;
// !! 回滚
curAns = backup;
while(winl <= br[block]){
del(a[winl++]);
}
}
qi ++;
}
}
for(int i = 1; i <= q; i ++){
cout << ans[i] << "\n";
}
}
signed main()
{
// freopen("in.txt", "rt", stdin);
// freopen("out.txt", "wt", stdout);
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
// int T=1; cin>>T; while(T--)
solve();
return 0;
}
多次查询区间最小众数
一个经典问题,可以利用只增回滚莫队解决。
假设我们维护好了某个集合的最小众数 minmode 与它出现的次数 maxcnt,以及每个数的词频。那么显然,往这个集合内添加一个数,minmode 和 maxcnt 这两个信息是很容易实时更新的;而若从这个集合中删除一个数,这两个信息便很难维护好。
思路与上一道模板题是一样的,这里就只贴代码了,偷个懒qwq。。。
#include <bits/stdc++.h>
using namespace std;
struct Query {
int l, r, k, id;
};
const int MAXN = 10001;
const int MAXM = 50001;
const int MAXB = 301;
int n, m;
int arr[MAXN];
Query query[MAXM];
int sorted[MAXN];
int cntv;
int blen, bnum;
int bi[MAXN];
int br[MAXB];
int cnt[MAXN];
int maxCnt;
int minMode;
int ans[MAXM];
bool QueryCmp(Query &a, Query &b) {
if (bi[a.l] != bi[b.l]) {
return bi[a.l] < bi[b.l];
}
return a.r < b.r;
}
int kth(int num) {
int left = 1, right = cntv, ret = 0;
while (left <= right) {
int mid = (left + right) >> 1;
if (sorted[mid] <= num) {
ret = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
return ret;
}
int force(int l, int r, int k) {
int mx = 0, who = 0;
for (int i = l; i <= r; i++) {
cnt[arr[i]]++;
}
for (int i = l; i <= r; i++) {
int num = arr[i];
if (cnt[num] > mx || (cnt[num] == mx && num < who)) {
mx = cnt[num];
who = num;
}
}
for (int i = l; i <= r; i++) {
cnt[arr[i]]--;
}
return mx >= k ? sorted[who] : -1;
}
void add(int num) {
cnt[num]++;
if (cnt[num] > maxCnt || (cnt[num] == maxCnt && num < minMode)) {
maxCnt = cnt[num];
minMode = num;
}
}
void del(int num) {
cnt[num]--;
}
void compute() {
for (int block = 1, qi = 1; block <= bnum && qi <= m; block++) {
maxCnt = 0;
minMode = 0;
fill(cnt + 1, cnt + cntv + 1, 0);
int winl = br[block] + 1, winr = br[block];
for (; qi <= m && bi[query[qi].l] == block; qi++) {
int jobl = query[qi].l;
int jobr = query[qi].r;
int jobk = query[qi].k;
int id = query[qi].id;
if (jobr <= br[block]) {
ans[id] = force(jobl, jobr, jobk);
} else {
while (winr < jobr) {
add(arr[++winr]);
}
int backupCnt = maxCnt;
int backupNum = minMode;
while (winl > jobl) {
add(arr[--winl]);
}
if (maxCnt >= jobk) {
ans[id] = sorted[minMode];
} else {
ans[id] = -1;
}
maxCnt = backupCnt;
minMode = backupNum;
while (winl <= br[block]) {
del(arr[winl++]);
}
}
}
}
}
void prepare() {
for (int i = 1; i <= n; i++) {
sorted[i] = arr[i];
}
sort(sorted + 1, sorted + n + 1);
cntv = 1;
for (int i = 2; i <= n; i++) {
if (sorted[cntv] != sorted[i]) {
sorted[++cntv] = sorted[i];
}
}
for (int i = 1; i <= n; i++) {
arr[i] = kth(arr[i]);
}
blen = (int)sqrt(n);
bnum = (n + blen - 1) / blen;
for (int i = 1; i <= n; i++) {
bi[i] = (i - 1) / blen + 1;
}
for (int i = 1; i <= bnum; i++) {
br[i] = min(i * blen, n);
}
sort(query + 1, query + m + 1, QueryCmp);
}
class Solution {
public:
vector<int> subarrayMajority(vector<int>& nums, vector<vector<int>>& queries) {
n = (int)nums.size();
m = (int)queries.size();
for (int i = 1; i <= n; i++) {
arr[i] = nums[i - 1];
}
for (int i = 1; i <= m; i++) {
query[i].l = queries[i - 1][0] + 1;
query[i].r = queries[i - 1][1] + 1;
query[i].k = queries[i - 1][2];
query[i].id = i;
}
prepare();
compute();
vector<int> ret(m);
for (int i = 1; i <= m; i++) {
ret[i - 1] = ans[i];
}
return ret;
}
};
P5906
Q:多次查询 \(a[l, r]\) 中相隔最远的两个相同数字的距离。
需要维护的信息:
- \(mostRight[x]\):数字 \(x\) 在当前区间内最右侧出现的位置。
- \(maxDist\):当前区间的答案
- \(First[x]\):在当前区间的左端点所在块右侧的整个区间中,数字 \(x\) 最左侧出现的位置。
具体实现见 code。
只删回滚莫队
P4137 (模板)
Q:静态多次查询区间 mex。
显然 mex 这个信息是适合只有删除操作的时候实时维护的。考虑对于某个可重集 \(S\),\(mex = v\)。若往集合中增加一个数,那么 \(mex\) 的更新还需要依靠其他更大的数的信息,不易实时维护;而若在集合中删除一个数,那么只要这个数在集合中被删没了,新的 \(mex\) 自然应该是原 \(mex\) 与这个数的最小值,否则 \(mex\) 不变。
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define fr first
#define se second
#define endl '\n'
#define pb push_back
//#define int long long
typedef long long ll;
typedef __int128 LL;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,int> pli;
const int maxn = 200005;
const int maxv = 200005;
const int maxb = 460;
int n, q, M;
int a[maxn], ori[maxn];
int cnt[maxv];
int bi[maxn], bl[maxb], br[maxb];
int mex;
int backup;
struct Queries{
int l, r, id;
}qry[200005];
int ans[200005];
// 只删回滚莫队的排序方式:先按左端点所在块号升序排序,再按右端点数值大小降序排序。
bool cmp(Queries a, Queries b){
if(bi[a.l] == bi[b.l]){
return a.r > b.r;
}
return bi[a.l] < bi[b.l];
}
// 对于只删回滚莫队:只有 del 更新 mex 的信息,而 add 不需要(后续由 backup 恢复 mex)
void del(int x){
if(--cnt[x] == 0){
mex = min(mex, x);
}
}
void add(int x){
cnt[x] ++;
}
void solve()
{
cin >> n >> q;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
const int blen = sqrt(n);
const int bcnt = (n + blen - 1) / blen;
for(int i = 1; i <= n; i ++){
bi[i] = (i - 1) / blen + 1;
}
for(int i = 1; i <= bcnt; i ++){
bl[i] = (i - 1) * blen + 1;
br[i] = min(n, i * blen);
}
for(int i = 1; i <= q; i ++){
int l, r;
cin >> l >> r;
qry[i] = {l, r, i};
}
sort(qry + 1, qry + q + 1, cmp);
// 先处理好整个区间
for(int i = 1; i <= n; i ++){
cnt[a[i]] ++;
}
mex = 0;
while(cnt[mex]) mex ++;
int winl = 1, winr = n; // 初始时窗口是整个序列
// 按照块的顺序遍历,依次解决每个查询
for(int block = 1, qi = 1; block <= bcnt && qi <= q; block ++){
while(winl < bl[block]){ // 先将窗口左端点更新到当前查询区间左端点所在块的左边界
del(a[winl++]);
}
int before = mex; // !!需要先备份下来整体的 mex,以便后续恢复
while(qi <= q && bi[qry[qi].l] == block){
auto [l, r, id] = qry[qi];
while(winr > r){
del(a[winr--]);
}
backup = mex; // 备份
while(winl < l){
del(a[winl++]);
}
ans[id] = mex;
mex = backup;
while(winl > bl[block]){
add(a[--winl]);
}
qi ++;
}
// !!每解决完一个块中的查询,需要将窗口右边界恢复到 n
while(winr < n){
add(a[++winr]);
}
// !!mex 信息恢复,与 backup 原理一致
mex = before;
}
for(int i = 1; i <= q; i ++){
cout << ans[i] << "\n";
}
}
signed main()
{
// freopen("in.txt", "rt", stdin);
// freopen("out.txt", "wt", stdout);
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
// int T=1; cin>>T; while(T--)
solve();
return 0;
}
P8078
先鸽着,以后补~
树上莫队
SP10707 (模板)
多次查询树上路径 \((u, v)\) 中的不同权值种类数。
#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define fr first
#define se second
#define endl '\n'
#define pb push_back
//#define int long long
typedef long long ll;
typedef __int128 LL;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,int> pli;
const int maxn = 40005;
int n, m;
int a[maxn];
vector<int> G[maxn];
int dep[maxn], fa[maxn][18];
int st[maxn]; // st[u] 表示结点 u 在括号序中的开始结点;
int ed[maxn]; // ed[u] 表示结点 u 在括号序中的结束结点;
int seg[maxn<<1]; // seg[i] 表示括号序为 i 的代表结点
int cntd; // 与dfn中的tim类似,在dfs过程中实时递增
int bi[maxn<<1]; // 对括号序seg作序列分块与莫队操作
// 窗口信息
// 在窗口中,出现一次的数计入,出现两次的数不计,因此用 vis 数组表示每个结点的翻转性
bool vis[maxn];
int cnt[maxn];
int kind=0;
int ans[100005];
struct Queries{
int l, r, lca, id;
}qry[100005];
// 普通莫队排序
bool cmp(Queries a, Queries b){
if(bi[a.l] == bi[b.l]) return a.r < b.r;
return bi[a.l] < bi[b.l];
}
void dfs(int u, int father){ // 初始化
dep[u] = dep[father] + 1;
fa[u][0] = father;
// 刚到达u时计入u的开始序
seg[++cntd] = u;
st[u] = cntd;
for(int i = 1; i <= 15; i ++){
fa[u][i] = fa[fa[u][i - 1]][i - 1];
}
for(auto v : G[u]){
if(v == father) continue;
dfs(v, u);
}
// 回溯时计入u的结束序
seg[++cntd] = u;
ed[u] = cntd;
}
int getlca(int u, int v){
if(dep[u] < dep[v]) swap(u, v);
for(int i = 15; i >= 0; i --){
if(dep[fa[u][i]] >= dep[v]){
u = fa[u][i];
}
}
if(u == v) return v;
for(int i = 15; i >= 0; i --){
if(fa[u][i] != fa[v][i]){
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}
void invert(int u){ // 翻转,处理结点u时根据vis[u]来决定操作的正逆性
int val = a[u];
if(vis[u]){ // 删除
if(--cnt[val] == 0){
kind --;
}
}
else{
if(++cnt[val] == 1){
kind ++;
}
}
vis[u] ^= 1;
}
void solve()
{
cin >> n >> m;
vector<int> alls;
for(int i = 1; i <= n; i ++){
cin >> a[i];
alls.pb(a[i]);
}
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
map<int,int> mp;
for(int i = 0; i < alls.size(); i ++) mp[alls[i]] = i + 1;
for(int i = 1; i <= n; i ++) a[i] = mp[a[i]];
for(int i = 1; i <= n - 1; i ++){
int u, v;
cin >> u >> v;
G[u].pb(v); G[v].pb(u);
}
dfs(1, 0);
const int blen = sqrt(cntd);
for(int i = 1; i <= n; i ++){
bi[i] = (i - 1) / blen + 1;
}
for(int i = 1; i <= m; i ++){
int u, v;
cin >> u >> v;
if(st[v] < st[u]) swap(u, v);
int lca = getlca(u, v);
if(u == lca){
qry[i] = {st[u], st[v], 0, i};
}
else{
qry[i] = {ed[u], st[v], lca, i};
}
}
sort(qry + 1, qry + 1 + m, cmp);
int winl = 1, winr = 0;
for(int i = 1; i <= m; i ++){
auto [l, r, lca, id] = qry[i];
while(winl > l){
invert(seg[--winl]);
}
while(winr < r){
invert(seg[++winr]);
}
while(winl < l){
invert(seg[winl++]);
}
while(winr > r){
invert(seg[winr--]);
}
if(lca > 0){
invert(lca);
}
ans[id] = kind;
if(lca > 0){
invert(lca);
}
}
for(int i = 1; i <= m; i ++){
cout << ans[i] << '\n';
}
}
signed main()
{
// freopen("in.txt", "rt", stdin);
// freopen("out.txt", "wt", stdout);
ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
// int T=1; cin>>T; while(T--)
solve();
return 0;
}
P4074
树上带修莫队。
依旧以后再补~









浙公网安备 33010602011771号