所有区间问题的套路
改题改题改题!!!做完题一定要好好改真的会继续两次NoNoNo继续n次遇见它!!!
C. 完美子图
线段树做法:考虑把一个区间向右拓展,新增加的一个位置对以前所有处理过的区间的影响,当然对自己这一个长度的影响是1,第一层循环枚举R,新加入的数能对过去的区间会造成影响只有两种情况:加入的新数成为了某段以i为右边界的区间的最大值或最小值,这个可以用单调栈来维护。
把图转成二维的之后问题就变成了max-min=r-l,但因为这是一个排列(代表没有重复),对于所有区间来说max-min>=r-l一定成立,所以合法的区间就是l+max-min的最小值=r,线段树上维护了l+max-min的最小值以及满足这个最小值的区间个数,建树的时候把 l 先放进去,长度只有1的时候max-min=0,正好是初始状态,更新时,(以最大值为例)新的最大值是a[i],被他影响到的上一个最大值是a[mx[t_mx]],虽然还可能有更多的被影响,但是要在每次出栈是更新因为“过去的最大值”变化了,而最大值的增量就是a[i]-a[mx[t_mx]],因为lazy放的是对最小值的更新所以当然不用*区间长度。
因为对于一个区间来说,max-min=r-l是它的最小情况,所以左右子树向上合并时,min相同才可能同时合法,min大的那个绝对不合法(已有比它小的它就不可能最小了),同样query的时候也不能把左右直接相加,分类讨论一下就好了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 5e4 + 2; const ll mod = 998244353; const int INF = 2147483647; int n, a[maxn], mx[maxn], mi[maxn], t_mx, t_mi; ll ans; inline int read() { int x = 0, f = 1; char ch = getchar(); while(ch > '9' || ch < '0') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } struct node { int ls, rs, cnt, min, lazy; }tree[maxn<<2]; int tot; #define lson tree[rt].ls #define rson tree[rt].rs void pushup(int rt) { if(tree[lson].min == tree[rson].min) { tree[rt].min = tree[lson].min; tree[rt].cnt = tree[lson].cnt+tree[rson].cnt; return; } if(tree[lson].min > tree[rson].min) { tree[rt].min = tree[rson].min; tree[rt].cnt = tree[rson].cnt; return; } tree[rt].min = tree[lson].min; tree[rt].cnt = tree[lson].cnt; } void pushdown(int rt) { if(tree[rt].lazy) { int lz = tree[rt].lazy; tree[rt].lazy = 0; tree[lson].lazy += lz; tree[rson].lazy += lz; tree[lson].min += lz; tree[rson].min += lz; } } int build(int rt, int l, int r) { if(!rt) rt = ++tot; if(l == r) { tree[rt].cnt = 1; tree[rt].min = l; return rt; } int mid = (l + r) >> 1; tree[rt].ls = build(lson, l, mid); tree[rt].rs = build(rson, mid+1, r); pushup(rt); return rt; } void update(int rt, int l, int r, int L, int R, int val) { if(L <= l && r <= R) { tree[rt].lazy += val; tree[rt].min += val; return; } pushdown(rt); int mid = (l + r) >> 1; if(L <= mid) update(lson, l, mid, L, R, val); if(R > mid) update(rson, mid+1, r, L, R, val); pushup(rt); } struct node2 { int id, v; node2(){} node2(int x, int y) {id = x, v = y;} }; node2 query(int rt, int l, int r, int L, int R) { if(L <= l && r <= R) { return node2(tree[rt].cnt, tree[rt].min); } pushdown(rt); int mid = (l + r) >> 1; node2 left, right; left.v = right.v = 1e9; if(L <= mid) left = query(lson, l, mid, L, R); if(R > mid) right = query(rson, mid+1, r, L, R); if(left.v < right.v) return left; if(right.v < left.v) return right; left.id += right.id; return left; } int main() { n = read(); for(int i=1; i<=n; i++) { int x = read(), y = read(); a[x] = y; } tot = 1; int root = build(1, 1, n); for(int i=1; i<=n; i++) { while(t_mx > 0 && a[mx[t_mx]] < a[i]) { update(root, 1, n, mx[t_mx-1]+1, mx[t_mx], a[i]-a[mx[t_mx]]); t_mx--; } mx[++t_mx] = i; while(t_mi > 0 && a[mi[t_mi]] > a[i]) { update(root, 1, n, mi[t_mi-1]+1, mi[t_mi], a[mi[t_mi]]-a[i]); t_mi--; } mi[++t_mi] = i; node2 cat = query(1, 1, n, 1, i); if(cat.v == i) ans += (ll)cat.id; } printf("%lld\n", ans); return 0; }
——感谢caorong
还可以用cdq分治:在合并的时候还是把区间想像成左右两部分,分类讨论一下:两个最值都在左; 都在右; min在左,max在右;min在右,max在左4种情况,也就是每次考虑跨过中点的区间答案,细节注释在了代码里。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; typedef unsigned long long ll; const int maxn = 50003; const ll mod = 998244353; const int INF = 0x7ffffff; int a[maxn], Min[maxn], Max[maxn], cnt[maxn<<1]; int ans, n; inline int read() { int x = 0, f = 1; char ch = getchar(); while(ch > '9' || ch < '0') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } void solve(int l, int r) { if(l == r) { ans++; return; } int mid = (l + r) >> 1; solve(l, mid); solve(mid+1, r); Min[mid] = Max[mid] = a[mid]; Min[mid+1] = Max[mid+1] = a[mid+1]; for(int i=mid-1; i>=l; i--) { Min[i] = min(Min[i+1], a[i]); Max[i] = max(Max[i+1], a[i]); } for(int i=mid+2; i<=r; i++) { Min[i] = min(Min[i-1], a[i]); Max[i] = max(Max[i-1], a[i]); } //两个最值都在左区间, for(int i=mid; i>=l; i--) { int j=i+Max[i]-Min[i];//如果通过最值找到的右端点合法 if(j<=r && j>mid && Max[i]>Max[j] && Min[i]<Min[j]) ans++; } for(int j=mid+1; j<=r; j++) { int i=j-Max[j]+Min[j]; if(i>=l && i<=mid && Max[i]<Max[j] && Min[i]>Min[j]) ans++; } int j = mid+1, k = mid+1; for(int i=mid; i>=l; i--)//左小右大 { while(j<=r && Min[j]>Min[i])//j满足左小,j是mid+1到右边界的区间,而不是右端点 { cnt[Max[j]-j+n]++;//在干嘛?所以j是一个符合条件的区间,Max[j]-j==Min[i]-i j++; } //Max是单调的,越接近越可能会合法,和上面的Min不同,它反向了 //j不变,因为Min[i]是区间最小值,只有可能变得更小或不变,而变得更小就使得j的范围更大 while(k<j && Max[i]>Max[k])//为了避免重复,一定要满足右大,可是为什么要比j小?而且,k是右区间, //这是在清理重复的情况,把左大的删掉 { cnt[Max[k]-k+n]--; k++; } //同上,k不需要清空 ans += cnt[Min[i]-i+n];//为了满足区间长度的条件,上面存的只是可能合法的(左小右大的) } while(k<j) { cnt[Max[k]-k+n]--;//其实就是清空数组的意思,memset会TLE k++; } j = mid, k = mid; for(int i=mid+1; i<=r; i++) { while(j>=l && Min[i]<Min[j]) { cnt[Max[j]+j]++; j--; } while(k>j && Max[k]<Max[i]) { cnt[Max[k]+k]--; k--; } ans += cnt[Min[i]+i];//Max[i]-Min[j]==j-i,移项 } while(k>j) { cnt[Max[k]+k]--; k--; } } int main() { n = read(); for(int i=1; i<=n; i++) { int x = read(), y = read(); a[x] = y; } solve(1, n); printf("%d", ans); return 0; }
B. 任意模数快速插值
未完待续……
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 5e5 + 2; const ll mod = 998244353; const int INF = 2147483647; const int lim = 1e4 + 1; int n, a[maxn], Max[maxn], Min[maxn], p[maxn]; ll ans, sum[maxn]; inline int read() { int x = 0, f = 1; char ch = getchar(); while(ch > '9' || ch < '0') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } void solve(int l, int r) { if(l == r) { ans = (ans+(ll)a[l]*a[l]%mod)%mod; return; } int mid = (l + r) >> 1; solve(l, mid); solve(mid+1, r); Max[mid] = Min[mid] = a[mid]; Max[mid+1] = Min[mid+1] = a[mid+1]; for(int i=mid-1; i>=l; i--) { Max[i] = max(Max[i+1], a[i]); Min[i] = min(Min[i+1], a[i]); } for(int i=mid+2; i<=r; i++) { Max[i] = max(Max[i-1], a[i]); Min[i] = min(Min[i-1], a[i]); } for(int i=mid,j=mid+1; i>=l; i--) { while(j<=r && Max[i]>=Max[j] && Min[i]<=Min[j]) j++; ans = (ans+(ll)Max[i]*Min[i]%mod*(j-mid-1)%mod)%mod; } for(int j=mid+1,i=mid; j<=r; j++) { while(i>=l && Max[i]<Max[j] && Min[i]>Min[j]) i--; ans = (ans+(ll)Max[j]*Min[j]%mod*(mid-i)%mod)%mod; } for(int i=mid,j=mid+1; i>=l; i--) { while(j<=r && Min[i]<=Min[j]) j++; p[i] = j; } sum[mid] = 0; for(int i=mid,j=mid+1; i>=l; i--) { while(j<=r && Max[i]>=Max[j]) { sum[j] = (sum[j-1]+Min[j])%mod; j++; } if(j-1 >= p[i])//j-1是因为j在最后多减了一个 { ans = (ans+(ll)Max[i]*(sum[j-1]-sum[p[i]-1])%mod)%mod;//sum[p[i]-1]是为了保留区间左端点 } } for(int j=mid+1,i=mid; j<=r; j++) { while(i>=l && Min[i]>Min[j]) i--; p[j] = i; } sum[mid+1] = 0; for(int j=mid+1,i=mid; j<=r; j++) { while(i>=l && Max[i]<Max[j]) { sum[i] = (sum[i+1]+Min[i])%mod; i--; } if(i+1 <= p[j]) { ans = (ans+(ll)Max[j]*(sum[i+1]-sum[p[j]+1])%mod)%mod; } } } int main() { n = read(); for(int i=1; i<=n; i++) { a[i] = read(); } solve(1, n); printf("%lld\n", (ans%mod+mod)%mod); return 0; }
C. 有趣的区间问题
关于代码里指针的移动条件,由于mn1和mx1是由上一步的若干个a[p1]得到的,它起不到限制作用,所以可以删掉。但是下面这个不能删,因为用了一个"||"上一步满足的条件并不确定,就有必要取一下min和max了。
eq数组是现算的不需要用memset来更新,但是不要忘了eq[mid] = 0。
如果左边最小值小于右边最小值,以左边的最小值为标准找到右边最大值与它相等的个数。[mid+1, p1]是闭区间,p1-(mid+1)+1 = p1-mid. (p1, p2]左开右闭,左开的实现就是cnt相减的时候减没了左端点,右闭在指针移动时形成。(p2, r]左开右闭,把p2的前缀和去掉了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//鹤的 #include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 1e6 + 5; const int logV = 65; const ll inf = 0x3f3f3f3f3f3f3f3f; int n, cnt1[logV], cnt2[logV], cnt3[logV], cnt4[logV], eq[maxn]; ll a[maxn], ans; #define Clear(a) memset(a, 0, sizeof(a)) #define popc __builtin_popcountll //我知道我赛时为什么没分了!因为我用的是__builtin_popcount没开ll!!! inline ll read() { ll x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } return x * f; } void solve(int l, int r) { if(l > r) return; if(l == r) {ans++; return;} int mid = (l + r) >> 1; Clear(cnt1), Clear(cnt2), Clear(cnt3), Clear(cnt4); eq[mid] = 0; //预处理中点必选,右区间的合法个数 for(ll i=mid+1,mn=a[mid+1],mx=a[mid+1]; i<=r; i++,mn=min(mn,a[i]),mx=max(mx,a[i])) { eq[i] = eq[i-1] + (popc(mn) == popc(mx)); } ll mn = inf, mx = -inf, mn1 = inf, mx1 = -inf, mn2 = inf, mx2 = -inf; //从大到小枚举左端点i,对于一个固定的i,在[m+1,r]之间一定会存在两个分界点p1,p2 //把右区间分为3部分,分类讨论max和min都在那里取到 //随着i递减,max和min都在左端/右端取到都会变难,p1,p2都是递增的 //第1种和第3种的贡献:右端点用预处理的,左端点直接距离*合法性 for(int i=mid,p1=mid,p2=mid; i>=l; i--) { mn = min(mn, a[i]); mx = max(mx, a[i]); while(p1<r && mn<=min(mn1,a[p1+1]) && mx>=max(mx1,a[p1+1])) { p1++; mn1 = min(mn1, a[p1]); mx1 = max(mx1, a[p1]); cnt1[popc(mn1)]++; cnt2[popc(mx1)]++; } while(p2<r && (mn<=min(mn2,a[p2+1])||mx>=max(mx2,a[p2+1])))//包含了第一种情况 { p2++; mn2 = min(mn2, a[p2]); mx2 = max(mx2, a[p2]); cnt3[popc(mn2)]++; cnt4[popc(mx2)]++; } ans += (popc(mn)==popc(mx))*(p1-mid)+eq[r]-eq[p2]; //讨论是max还是min在左区间取到 if(mn<=mn2) ans += cnt4[popc(mn)]-cnt2[popc(mn)];//左端最小值小于右端最小值 //最小值在左边取,最大值在右边取,用全部减掉最大值也在左边取的个数,就是最大值和最小值pop相等的数目 else ans += cnt3[popc(mx)]-cnt1[popc(mx)]; //关于我本来打算开一棵可持久化权值线段树来找个数这件事…… } solve(l, mid); solve(mid+1, r); } int main() { n = read(); for(int i=1; i<=n; i++) a[i] = read(); solve(1, n); printf("%lld\n", ans); return 0; }
C. english
和上面的题大概类型不太一样,虽然也是所有区间,但主要是问题的转化和异或的应用,主要算法是01Trie
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/* 好吧,下文的注释都是我自己的胡思乱想,一问题解的作者就发现全都想错了 */ #include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 1e5 + 100; const int mod = 1e9 + 7; const int INF = 2147483647; int L[maxn], R[maxn], st[maxn], top, trie[maxn*22][2], tot=1; int rat[maxn*22], n_1[maxn][22], n_0[maxn][22], n, opt, A[maxn], root[maxn]; inline int read() { int x = 0, f = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') { f = -1; } ch = getchar(); } while(ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch^48); ch = getchar(); } return x * f; } //对每一个区间维护一棵01Trie树 = 来一个可持久化Trie inline void Insert(int a) { int pre = root[a-1]; root[a] = ++tot; int now = tot; for(int i=20; i>=0; i--) { if(pre) { trie[now][0] = trie[pre][0]; trie[now][1] = trie[pre][1]; } bool op = ((A[a]&(1<<i))>0); rat[now] = rat[pre] + 1;//rat[]'s use?一定会多1吗? //一定会多1,因为now是新建的节点,多加了一个数??大小关系怎么确定? //rat代表如果更大的话,即“子树的大小”,但这里的子树不是节点,而是now的左子树上有几个“边缘” //也就是从根到now这个节点代表的这一位都相同,在now的后一位更小了的数的数目 //不过这大概是一种错误的理解,这样的话ans显然加多了??? //只是一个点怎么会有大小之分??所以ret[now]存的是经过now这个点的数的个数 trie[now][op] = ++tot; //同时转向子树:为了省空间,只开必要的节点,而不是新建一条链 now = tot; if(pre) pre = trie[pre][op]; } rat[now] = rat[pre] + 1;//那这里为什么要重复? } inline int Query(int now, int a, int b)//A[a]^? > A[b]满足条件的?有几个 { int ans = 0; for(int i=20; i>=0; i--) { bool x = ((A[a]&(1<<i))>0), y = ((A[b]&(1<<i))>0); if(!y) { ans = ans+rat[trie[now][x^1]]; now = trie[now][x]; } else //在同一位上,y=1代表a已经没有了比b大的机会?但这只是拆开的单独一位啊 { now = trie[now][x^1]; } if(!now) break; } return ans; } int main() { n = read(); opt = read(); root[0] = 1; for(int i=1; i<=n; i++) { A[i] = read(); while(top > 0 && A[i]>=A[st[top]]) { R[st[top]] = i-1; top--; } L[i] = st[top] + 1; st[++top] = i; if(opt == 1 || opt == 3) { for(int j=0; j<=20; j++) { //分别记录第j位为1和第j位为0的前缀和 n_1[i][j] = n_1[i-1][j]; n_0[i][j] = n_0[i-1][j]; if((1<<j)&A[i]) n_1[i][j]++; else n_0[i][j]++; } } if(opt == 2 || opt == 3) { Insert(i); } } A[n+1] = INF; while(top > 0 && A[n+1]>=A[st[top]]) { R[st[top]] = n; top--; } ll ans1 = 0, ans2 = 0; for(int i=1; i<=n; i++) { if(i-L[i] < R[i]-i) { if(opt == 1 || opt == 3) { for(int j=L[i]; j<=i; j++)//不对啊,为什么两边可以同时取等? { for(int k=0; k<=20; k++) { if((1<<k)&A[j]) //因为没有影响,i已经是相反的了 { ans1 = (ans1+1ll*(n_0[R[i]][k]-n_0[i-1][k])*(1<<k)%mod*A[i]%mod)%mod; } else //它也取等一样解释?? { ans1 = (ans1+1ll*(n_1[R[i]][k]-n_1[i-1][k])*(1<<k)%mod*A[i]%mod)%mod; } } } } if(opt == 2 || opt == 3) { for(int j=L[i]; j<=i; j++) { //什么意思?这就是题解上说的启发式合并?? //右区间有多少个数满足 v^a[i] > a[x],x是最大值,i是循环的右区间 ans2 = (ans2+(ll)A[i]*(Query(root[R[i]], j, i)-Query(root[i-1], j, i)))%mod; } } } else { if(opt == 1 || opt == 3) { for(int j=i; j<=R[i]; j++) { for(int k=0; k<=20; k++) { if((1<<k)&A[j]) { ans1 = (ans1+1ll*(n_0[i][k]-n_0[L[i]-1][k])*(1<<k)%mod*A[i]%mod)%mod; } else { ans1 = (ans1+1ll*(n_1[i][k]-n_1[L[i]-1][k])*(1<<k)%mod*A[i]%mod)%mod; } } } } if(opt == 2 || opt == 3) { for(int j=i; j<=R[i]; j++) { ans2 = (ans2+(ll)A[i]*(Query(root[i], j, i)-Query(root[L[i]-1], j, i)))%mod; } } } } if(opt == 1 || opt == 3) printf("%lld\n", ans1); if(opt == 2 || opt == 3) printf("%lld\n", ans2); return 0; }