数据结构进阶
前缀和 & 差分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T9Fu0mpJ-1636469078728)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210924154834487.png)]
前缀和
给定一个长度大小为N的数组,进行静态的M轮查询。每轮查询一个连续区间l~r,问区间元素和。
前缀和的变形
数学
乘积,满秩矩阵链乘法,卷积
A-智乃酱的区间乘积_牛客竞赛数据结构专题班前缀和练习题 (nowcoder.com)
操作的相互抵消
广义的“前缀和”运算是指连续的进行若干次操作,产生一个叠加影响,如果这种影响可以通过某种反向操作“撤销”。
F-牛牛的猜球游戏_牛客竞赛数据结构专题班前缀和练习题 (nowcoder.com)
前缀和思想:
做某一段区间里的变换只需要考虑 a [ l − 1 ] a[l-1] a[l−1]与 a [ r ] a[r] a[r]中状态之间的变化情况
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 1e5 + 50;
int n, m;
int a[MAXN][11];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 0; i < 10; i++) a[0][i] = i;
for(int i = 1; i <= n; i++)
{
for(int j = 0; j < 10; j++) a[i][j] = a[i - 1][j];
int l, r;
scanf("%d%d", &l, &r);
swap(a[i][l], a[i][r]);
}
for(int j = 1; j <= m; j++)
{
int l, r;
scanf("%d%d", &l, &r);
int pos[11];
for(int i = 0; i < 10; i++) pos[a[l - 1][i]] = i;
for(int i = 0; i < 10; i++) printf("%d ", pos[a[r][i]]);
printf("\n");
}
return 0;
}
与DP & 矩阵的结合
前缀积:https://ac.nowcoder.com/acm/contest/19483/A
维护逆元
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 50, mod = 1e9 + 7;
ll a[MAXN], mul[MAXN];
ll f[MAXN], inf[MAXN];
int n, m;
int qmi(int a, int k)
{
int res = 1;
while(k)
{
if(k & 1) res = (ll) res * a % mod;
k >>= 1;
a = (ll) a * a % mod;
}
return res;
}
void init()
{
f[0] = inf[0] = 1;
for(int i = 1; i <= n; i++)
{
f[i] = f[i - 1] * a[i] % mod;
inf[i] = inf[i - 1] * qmi(a[i], mod - 2) % mod;
}
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%lld", &a[i]);
init();
for(int i = 1; i <= m; i++)
{
int l, r;
scanf("%d%d", &l, &r);
printf("%lld\n", f[r] * inf[l - 1] % mod);
}
return 0;
}
E-智乃酱的双塔问题_牛客竞赛数据结构专题班前缀和练习题 (nowcoder.com)
题意描述
左右有两个高度为n个塔,对于每个塔可以从下一层直接到上一层。对于每层(不包括顶层)额外有一个楼梯要么是从左塔上到右塔,要么是从右塔往上走一层到左塔。
每次询问从左边或右边塔的第 i 层走到左边或右边的第 j 层的方案数(i < j)。
d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] + d p [ i − 1 ] [ 1 ] ∗ [ s [ i − 1 ] ≠ ′ / ′ ] d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] + d p [ i − 1 ] [ 0 ] ∗ [ s i − 1 = ′ / ′ ] 考 虑 用 矩 阵 表 示 这 个 转 移 方 程 [ d p [ i ] [ 0 ] d p [ i ] [ 1 ] ] = [ d p [ i − 1 ] [ 0 ] d p [ i − 1 ] [ 1 ] ] × [ 1 s i − 1 = ′ / ′ s i − 1 ≠ ′ / ′ 1 ] dp[i][0]=dp[i-1][0]+dp[i-1][1]*[s[i-1] \ne '/'] \\ dp[i][1]=dp[i-1][1]+dp[i-1][0]*[s_{i-1}='/'] \\ 考虑用矩阵表示这个转移方程 \\ [ \ dp[i][0] \ \ \ \ dp[i][1] \ ]=[ \ dp[i-1][0] \ \ \ \ dp[i-1][1] \ ] \times \begin{bmatrix} 1 & s_{i-1}='/' \\ s_{i-1} \ne '/' & 1 \end{bmatrix} dp[i][0]=dp[i−1][0]+dp[i−1][1]∗[s[i−1]=′/′]dp[i][1]=dp[i−1][1]+dp[i−1][0]∗[si−1=′/′]考虑用矩阵表示这个转移方程[ dp[i][0] dp[i][1] ]=[ dp[i−1][0] dp[i−1][1] ]×[1si−1=′/′si−1=′/′1]
最后答案需要求出矩阵的逆, m a t r i x [ r ] ∗ i n v ( m a t r i x [ l ] ) matrix[r] * inv(matrix[l]) matrix[r]∗inv(matrix[l])
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define pii pair<int,int>
#define pll pair<ll,ll>
#define pli pair<ll,int>
#define Min(a,b,c) min(a,min(b,c))
#define Max(a,b,c) max(a,max(b,c))
typedef long long ll;
typedef unsigned long long ull;
const double pi = 3.141592653589793;
const double eps = 1e-8;
const int INF = 0x3f3f3f3f;
const int N = 100010;
const ll mod = 1000000007;
struct mat
{
ll a[2][2];
mat() {memset(a, 0, sizeof(a));}
mat(int a1, int a2, int a3, int a4)
{
a[0][0] = a1;
a[0][1] = a2;
a[1][0] = a3;
a[1][1] = a4;
}
};
int n, m;
char s[N];
mat sum[N], A(1, 1, 0, 1), B(1, 0, 1, 1);
ll qmi(ll a, ll b)
{
ll ans = 1;
for ( ; b; b >>= 1)
{
if (b & 1) ans = ans * a % mod;
a = a * a % mod;
}
return ans;
}
ll inv(ll x)
{
return qmi(x, mod - 2);
}
void mul(ll a[][2], ll b[][2])
{
ll c[2][2];
memset(c, 0, sizeof(c));
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
for (int k = 0; k < 2; k++)
c[i][j] = (c[i][j] + a[i][k] * b[k][j] % mod) % mod;
memcpy(a, c, sizeof(c));
}
void inv(ll a[][2])
{
int n = 2, is[2], js[2];
memset(is, 0, sizeof(is));
memset(js, 0, sizeof(js));
for (int k = 0; k < n; k++)
{
for (int i = k, j; i < n; i++)
{
for (j = k; j < n && !a[i][j]; j++);
is[k] = i, js[k] = j;
}
for (int i = 0; i < n; i++)
swap(a[k][i], a[is[k]][i]);
for (int i = 0; i < n; i++)
swap(a[i][k], a[i][js[k]]);
if (!a[k][k])
{
puts("No Solution");
exit(0);
}
a[k][k] = inv(a[k][k]);
for (int j = 0; j < n; j++)
if (j != k)
(a[k][j] *= a[k][k]) %= mod;
for (int i = 0; i < n; i++)
if (i != k)
{
ll temp = a[i][k];
a[i][k] = 0;
for(int j = 0; j < n; ++j)
(a[i][j] += mod - a[k][j] * temp % mod) %= mod;
}
}
for (int k = n - 1; k >= 0; k--)
{
for (int i = 0; i < n; i++)
swap(a[js[k]][i], a[k][i]);
for (int i = 0; i < n; i++)
swap(a[i][is[k]],a[i][k]);
}
}
int main()
{
scanf("%d%d", &n, &m);
scanf("%s", s + 1);
sum[0] = mat(1, 0, 0, 1);
for (int i = 1; i < n; i++)
{
sum[i] = sum[i - 1];
if (s[i] == '/') mul(sum[i].a, A.a);
else mul(sum[i].a, B.a);
}
while (m--)
{
int l, r, pl, pr;
scanf("%d%d%d%d", &l, &r, &pl, &pr);
mat ans = sum[l - 1];
inv(ans.a); mul(ans.a, sum[r - 1].a);
printf("%d\n", ans.a[pl][pr]);
}
return 0;
}
前缀置换:https://ac.nowcoder.com/acm/contest/19483/F
前缀矩阵积:https://ac.nowcoder.com/acm/contest/19483/E
高阶前缀和(等学完FFT,NTT再学,绝绝子)
对于数组a,定义a数组的前缀和为一阶前缀和,定义a数组前缀和的前缀和为二阶前缀和。
如果想求a数组的k阶前缀和,有没有快一点的方法。通过计算贡献,这个贡献是一个组合数贡献。
卷积:两个数组 a , b a,b a,b
c k = a i ∗ b j ( i + j = k − 1 ) c_k=a_i*b_j(i + j = k -1) ck=ai∗bj(i+j=k−1)
N T T NTT NTT可以把上述的复杂度降到 O ( n l o g n ) O(nlogn) O(nlogn)
高维前缀和(SOSDP)
Sum over Subsets ,又被叫做子集前缀和。
二维(容斥)
s u m [ i ] [ j ] = a [ i ] [ j ] + s u m [ i ] [ j − 1 ] + s u m [ i − 1 ] [ j ] − s u m [ i − 1 ] [ j − 1 ] sum[i][j] = a[i][j] + sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1] sum[i][j]=a[i][j]+sum[i][j−1]+sum[i−1][j]−sum[i−1][j−1]
三维(容斥8项)
https://ac.nowcoder.com/acm/contest/19483/B
高维
有 n n n个物品,问其所有子集的和
状态压缩的思想
a [ 1 < < 20 ] a[1 << 20] a[1<<20]
枚举状态和每一位,对每一维做前缀和。
B-智乃酱的子集与超集_牛客竞赛数据结构专题班前缀和练习题 (nowcoder.com)
用二进制数维护一个集合
前缀和表示集合的子集,后缀和维护集合的超集
if(j & (1 << i)) pre[j] += pre[j ^ (1 << i)]; else suf[j] += suf[j ^ (1 << i)];
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
int n, m;
const int N = 21, M = 1e5 + 50;
ll a[N];
ll pre[1 << N], suf[1 << N];
int main()
{
cin >> n >> m;
for(int i = 0; i < n; i++)
{
scanf("%lld", &a[i]);
}
for(int j = 0; j < 1 << n; j++)
{
ll sum = 0;
for(int i = 0; i < n; i++)
{
if(j & (1 << i)) sum ^= a[i];
}
pre[j] = suf[j] = sum;
}
for(int i = 0; i < n; i++)
{
for(int j = 0; j < 1 << n; j++)
{
if(j & (1 << i)) pre[j] += pre[j ^ (1 << i)];
else suf[j] += suf[j ^ (1 << i)];
}
}
while(m--)
{
int num;
cin >> num;
ll sum = 0;
for(int i = 1; i <= num; i++)
{
int x;
scanf("%d", &x);
sum += (1 << (x - 1));
}
printf("%lld %lld\n", pre[sum], suf[sum]);
}
return 0;
}
贡献计算
G-牛牛的Link Power I_牛客竞赛数据结构专题班前缀和练习题 (nowcoder.com)
维护等差数列的两次前缀和
首先赋值有1的地方为1。
两次等差数列后可以发现当前的1对后面所有的1贡献
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int n;
string str;
const int N = 1e5 + 10, mod = 1e9 + 7;
typedef long long ll;
ll sum[N];
void pre_sum() {
for (int i = 1; i <= n; i ++ ) {
sum[i] += sum[i - 1];
if (sum[i] > mod) sum[i] -= mod;
}
}
int main() {
//这题考虑的是前面的1对后面的1的影响
cin >> n;
cin >> str;
for (int i = 0; i < n; i ++ ) {
if (str[i] == '1') sum[i + 1] = 1;//在1的后一个位置为1,因为后一个位置的距离才是1
}
pre_sum(); pre_sum();
//等差数列两次前缀和变成1
//1两次前缀和变成等差数列
//其实不止是1可以,如果是2的话,两次前缀和是差值为2的等差数列
//每个1的后面一位都为1,这样在每个1的后面一位都做二次前缀和,那个存在1的位置的sum值就是前面的1对这个1的影响
ll ans = 0;
for (int i = 0; i < n; i ++ ) {
if (str[i] == '1') ans += sum[i];
if (ans > mod) ans -= mod;
}
cout << ans << endl;
return 0;
}
差分
给定一个长度大小为N的数组,数组元素为整数,首先进行M次修改操作,每次给l~r的区间范围的元素同时加上一个数字x。M次操作后输出整个数组。
这个是差分数组的经典应用,同时也应该是大家新手期接触的第一个“计算贡献”的技巧。
[I-NOIP2013]积木大赛_牛客竞赛数据结构专题班前缀和练习题 (nowcoder.com)(思维)
[J-NOIP2018]道路铺设_牛客竞赛数据结构专题班前缀和练习题 (nowcoder.com)
题意:从平地起盖房子,改成 i d id id位置上的高度为 h [ i d ] h[id] h[id]
逆向思维,相当于把房子推平
保证每个位置上的高度为0,即差分数组为0
每次操作让一个正数减1,负数加1
答案为所有差分数组正数的和
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long ll;
const int MAXN = 1e5 + 50;
int n;
int a[MAXN], d[MAXN];
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= n + 1; i++) d[i] = a[i] - a[i - 1];
ll ans = 0;
for(int i = 1; i <= n + 1; i++)
{
if(d[i] > 0) ans += d[i];
}
printf("%lld\n", ans);
return 0;
}
[A-NOIP2012]借教室_牛客竞赛数据结构专题班树状数组、线段树练习题 (nowcoder.com)
差分+二分(对询问的次数进行二分)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 1e6 + 60;
int n, m;
int a[MAXN], tr[MAXN], c[MAXN], g[MAXN], gg[MAXN];
struct node{
int d, s, t;
}N[MAXN];
int lowbit(int x)
{
return x & (-x);
}
int sum(int x)
{
int ret = 0;
for(int i = x; i ; i -= lowbit(i))
{
ret += tr[i];
}
return ret;
}
void change(int x, int k)
{
for(int i = x; i <= n; i++)
{
tr[i] += k;
}
}
bool check(int x)
{
memset(g, 0, sizeof(g));
memcpy(gg, c, sizeof(gg));
for(int i = 1; i <= x; i++)
{
int s = N[i].s, t = N[i].t, d = N[i].d;
gg[s] -= d;
gg[t + 1] += d;
}
for(int i = 1; i <= n; i++)
{
g[i] = g[i - 1] + gg[i];
if(g[i] < 0) return false;
}
return true;
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
c[i] = a[i] - a[i - 1];
}
for(int i = 1; i <= m; i++)
{
int d, s, t;
scanf("%d%d%d", &d, &s, &t);
N[i] = {d, s, t};
}
int l = 1, r = m;
while(l < r)
{
int mid = (l + r) >> 1;
if(check(mid)) l = mid + 1;
else {
r = mid;
}
}
if(check(r)) printf("0\n");
else printf("-1\n%d\n", r);
return 0;
}
差分扩展
加上一个等差数列
维护差分数组的差分。
原数列: a a a: 0 0 0 0 0 1 2 3 4 5 6 0 0 0
差分数列: d d d: 0 0 0 0 0 1 1 1 1 1 1 -6 0 0
差分数组的差分 d d dd dd: 0 0 0 0 0 1 0 0 0 0 0 -7 0 0
数组1 0 0 0 0 0 0 0 维护两次前缀和就是等差数列
加上一个平方数列
维护差分数组的二次差分
原数列: a a a: 0 0 0 0 0 1 4 9 16 25 36 0 0 0
差分数列: d d d: 0 0 0 0 0 1 3 5 7 9 11 -36 0 0
差分数组的差分 d d dd dd: 0 0 0 0 0 1 2 2 2 2 2 -47 0 0
差分数组的二次差分 d d d ddd ddd: 0 0 0 0 0 1 1 0 0 0 0 -49 0 0
数组 1 1 0 0 0 0 0 0 开头 维护三次前缀和就是平方数组
更一般的情况
数学定理:最高次项为n次的n阶多项式做n+1阶差分后余项为常数。
如何利用这个定理,使我们的前缀和可以做区间加多项式?
https://ac.nowcoder.com/acm/contest/19483/D
树状数组和线段树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WlZeZZRx-1636469078731)(C:/Users/DELL/AppData/Roaming/Typora/typora-user-images/image-20210927161229829.png)]
树状数组
lowbit可以用来判断一个数是否是2的整次幂
return x && x == lowbit(x)
树状数组 c [ x ] c[x] c[x]表示从 x x x开始长度为 l o w b i t ( x ) lowbit(x) lowbit(x)的和
树状数组被理解成是“可以修改”的前缀和。使用时有什么局限性?
1、因为树状数组的本质是前缀和,所以依赖“前缀可减性”
如果维护一个区间最大值 [ l , r ] [l,r] [l,r],已知 [ 1 , r ] [1,r] [1,r]和 [ 1 , l − 1 ] [1,l-1] [1,l−1]的最大值,他不能直接相减(不能消除 [ 1 , l − 1 ] [1,l-1] [1,l−1]的影响)
2、树状数组的单点修改具有局限性,例如“带修改的前缀max,min”问题中,修改必须具有单调性。
维护区间 m a x max max,单点修改的时候不能往小的地方修改。
[B-SDOI2009]HH的项链_牛客竞赛数据结构专题班树状数组、线段树练习题 (nowcoder.com)
问一段区间中一个物品的种类数
思路:按询问右端点排序
看一个例子说明树状数组如何维护
1 2 3 4 3 2 1
1 0 0 0 0 0 0
1 1 0 0 0 0 0
1 1 1 0 0 0 0
1 1 1 1 0 0 0
1 1 0 1 1 0 0
1 0 0 1 1 1 0
0 0 0 1 1 1 1
遍历询问的右端点
若当前没有这个种类的东西,则该物品加一
若已经有了,则在上一个相同物品的地方-1,该物品放的地方+1。
查询前缀和即是最后的种类数。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 1e6 + 60;
int n, m;
int a[MAXN], tr[MAXN], last[MAXN];
int lowbit(int x)
{
return x & (-x);
}
void add(int x, int k)
{
for(int i = x; i <= n; i += lowbit(i))
{
tr[i] += k;
}
}
int sum(int x)
{
int ret = 0;
for(int i = x; i ; i -= lowbit(i))
{
ret += tr[i];
}
return ret;
}
struct query{
int l, r, id, ans;
bool operator<(const query &a)
{
return r < a.r;
}
}Q[MAXN];
bool cmp(query a, query b)
{
return a.id < b.id;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
scanf("%d", &m);
for(int i = 1; i <= m; i++)
{
int l, r;
scanf("%d%d", &l, &r);
Q[i] = {l, r, i};
}
sort(Q + 1, Q + m + 1);
int cnt = 1;
for(int i = 1; i <= m; i++)
{
int l = Q[i].l, r = Q[i].r;
while(cnt <= r)
{
int v = a[cnt];
if(!last[v]) {
add(cnt, 1);
last[v] = cnt;
}
else {
add(last[v], -1);
add(cnt, 1);
last[v] = cnt;
}
cnt++;
}
int ans = sum(r) - sum(l - 1);
Q[i].ans = ans;
}
sort(Q + 1, Q + 1 + m, cmp);
for(int i = 1; i <= m; i++) printf("%d\n", Q[i].ans);
return 0;
}
/*
7
1 2 3 4 3 2 1
4
1 4
3 5
1 7
3 6
*/
[C-HEOI2012]采花_牛客竞赛数据结构专题班树状数组、线段树练习题 (nowcoder.com)
同样求种类的题目
与上一题的不同之处在于,这道题两朵花才能采摘
同样进行排序
每一次更新的时候,让前前朵-1,前一朵+1。
注意线段树非0情况(死循环)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 2e6 + 100;
int n, c, m;
int a[MAXN], last[MAXN], nxt[MAXN], tr[MAXN];
struct node{
int l, r, id, ans;
bool operator<(const node &a)
{
if(r == a.r) return l < a.l;
return r < a.r;
}
}N[MAXN];
int lowbit(int x)
{
return x & (-x);
}
int sum(int x)
{
if(!x) return 0;
int ret = 0;
for(int i = x; i; i -= lowbit(i))
{
ret += tr[i];
}
return ret;
}
void add(int x, int k)
{
if(!x) return;
for(int i = x; i <= n; i += lowbit(i)) tr[i] += k;
}
bool cmp(node a, node b)
{
return a.id < b.id;
}
int main()
{
scanf("%d%d%d", &n, &c, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= m; i++)
{
int l, r;
scanf("%d%d", &l, &r);
N[i] = {l ,r, i};
}
sort(N + 1, N + 1 + m);
for(int i = 1; i <= n; i++)
{
nxt[i] = last[a[i]];
last[a[i]] = i;
}
int cnt = 1;
for(int i = 1; i <= m; i++)
{
int l = N[i].l, r = N[i].r;
while(cnt <= r)
{
add(nxt[nxt[cnt]], -1);
add(nxt[cnt], 1);
cnt++;
}
N[i].ans = sum(r) - sum(l - 1);
}
sort(N + 1, N + 1 + m, cmp);
for(int i = 1; i <= m; i++) printf("%d\n", N[i].ans);
return 0;
}
线段树
线段树中的对象:
- 线段树
- 区间(线段)
- 懒标记
区间对象
对于一个区间,或者说是线段树上的线段,一般都这样定义
struct{
int l,r;
int DATA;
int LAZY;
};
能否用线段树的表示:是否能够进行区间合并。
可以用线段树维护信息条件之一就是,区间信息满足可加性。
如果已知左侧区间元素和,右侧区间的元素和。能否知道他们和区间的区间和?
如果已知左侧区间元素的乘积,右侧区间的元素的乘积。能否知道他们和区间的区间的乘积?
如果是元素种类数呢? 不行!!!
线段树的区间修改(LazyTag)
修改函数是否有pushdown操作
这个是取决于修改效果的时序性。
对于简单问题来说,确实没有必要对懒标记做很复杂的设计,但是当需要维护的信息足够复杂,或者修改操作足够复杂,多种懒标记互相影响的时候,这仍然是有必要的。
最简单的例子,加乘线段树。
在复杂问题中,你至少要为lazytag设计如下三个函数
-
push_down()
-
cal_lazy()
(数学计算) -
tag_union()
势能线段树
势能(potential energy)是储存于一个系统内的能量,也可以释放或者转化为其他形式的能量。势能是状态量,又称作位能。势能不是属于单独物体所具有的,而是相互作用的物体所共有。
在物理学中势能的定义如上所述,在信息学中,也会引入一些物理概念来类比。
物理学中引入势能的目的是为了忽略复杂过程的影响。
信息学中引入势能的根本目的是相同的。
一个简单问题:两个小于C的数字求最大公约数gcd,时间复杂度是多少?
O(logC)啊
三个数呢?
还是O(logC)啊
N个数呢?
O(NlogC)?
答案是O(N+logC),有点反直觉对吧。
原因在于,我们在计算N个数的gcd时,首先令ans=a[1]。
然后顺次遍历整个数组,每次令ans=gcd(ans,a[i])。
ans在这个过程中只会单调的往下减少,不会上升。
在信息学中,势能被用于计算某一个过程,或者某一类过程时间复杂度的总和。
例如在刚才求解gcd的例子中,就可以定义整个gcd函数被调用的总势能为C。这个势能只会单调减少,并且减少的次数是logC次。
这个时候复杂度显然就是
总时间复杂度=数组循环遍历复杂度+gcd函数被调用的总复杂度
势能均摊复杂度是指把总的时间复杂度摊到操作次数或者循环次数上面。
这样做的原因是对于一个循环嵌套结构
for(i=1…N)
{
f(x)…
}
我们比起用O(总复杂度)=ΣO(f)这种和式的表示方法。
更喜欢使用O(总复杂度)=N*O(f)这种嵌套乘法原理的形式。
在N个数求gcd问题中,总时间复杂度为O(N+logC)
那么势能均摊复杂度就为O(N+logC)/N=O(1+logC/N)=O(1)
我们就称在N个数求gcd问题中,gcd函数的“势能均摊复杂度”为O(1)。
在线段树维护区间开平方问题中,我们能不能使用“懒标记”把区间的开平方操作给“懒”掉?为什么不可以?
因为区间中每个数字都是不相同的,就算知道了Σsum,也不知道开方以后的Σsum,所以区间开方不支持使用lazy的本质在于,无法使用懒标记快速更新整段区间信息。
实际上线段树中的数字经过多次开平方操作以后会变为1(一开始都是正整数的情况下)。
所以,如果把线段树当成普通的数组来用,暴力的开平方,直到所有数字都变成1再开始借助lazy tag
所谓势能线段树,是指在懒标记无法正常使用的情况下,暴力到叶子将线段树当成数组一样用进行修改。
这个时候的复杂度计算就不是很直观了,所以引入势能的概念。
每一个节点都有一个关于开根号操作的“势能”,然后开根号的时候势能一定是递减的。
注意引入势能分析是忽略过程,只考虑所有过程的总和。
即我们不管在一次暴力中到底一个树节点被访问了几次(单次操作的时间复杂度可能很大,可能是O(N))但是这些暴力的总量是势能上限。
D-势能线段树模板题一_牛客竞赛数据结构专题班势能线段树、李超线段树 (nowcoder.com)
区间开根号与区间求和
思路:任何数开根号10来次都变成1,所以暴力到叶结点,flag判断叶结点是否为1。
如过当前区间都是一,则修改的时候直接跳过该区间。
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<cmath>
using namespace std;
typedef long long ll;
const int MAXN = 2e5 + 50;
int n, m;
ll a[MAXN];
struct node{
int l, r;
ll sum;
bool flag;
}tr[MAXN * 4];
void pushup(int u)
{
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
tr[u].flag = tr[u << 1].flag && tr[u << 1 | 1].flag;
}
void build(int u, int l, int r)
{
tr[u] = {l, r};
if(l == r)
{
tr[u].sum = a[l];
tr[u].flag = (a[l] == 1 || a[l] == 0);
return;
}
tr[u].flag = 0;
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void change(int u, int l, int r)
{
if(l <= tr[u].l && tr[u].r <= r && tr[u].flag)
{
return;
}
else if(tr[u].l == tr[u].r)
{
tr[u].sum = sqrt(tr[u].sum);
if(tr[u].sum == 1) tr[u].flag = 1;
return;
}
int mid = (tr[u].l + tr[u].r) >> 1;
if(l <= mid) change(u << 1, l, r);
if(r > mid) change(u << 1 | 1, l, r);
pushup(u);
}
ll query(int u, int l, int r)
{
if(l <= tr[u].l && tr[u].r <= r)
{
return tr[u].sum;
}
int mid = (tr[u].l + tr[u].r) >> 1;
ll ans = 0;
if(l <= mid) ans += query(u << 1, l, r);
if(r > mid) ans += query(u << 1 | 1, l, r);
return ans;
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%lld", &a[i]);
build(1, 1, n);
for(int i = 1; i <= m; i++)
{
int op, l, r;
scanf("%d%d%d", &op, &l, &r);
if(op == 1) change(1, l, r);
else {
ll ans = query(1, l, r);
printf("%lld\n", ans);
}
}
return 0;
}
C-势能线段树模板题二_牛客竞赛数据结构专题班势能线段树、李超线段树 (nowcoder.com)
区间开平方,区间求和,区间修改
维护区间的最大值和最小值,当区间值都相等时,再进行修改。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;
struct node
{
int l,r;
ll sum,max,min;
ll lazp;
}t[N<<2];
void pushup(int k)
{
t[k].sum=t[k<<1].sum+t[k<<1|1].sum;
t[k].max=max(t[k<<1].max,t[k<<1|1].max);
t[k].min=min(t[k<<1].min,t[k<<1|1].min);
}
void f(int k,int v)
{
t[k].sum+=(t[k].r-t[k].l+1)*v;
t[k].max+=v;
t[k].min+=v;
t[k].lazp+=v;
}
void pushdown(int k)
{
f(k<<1,t[k].lazp);
f(k<<1|1,t[k].lazp);
t[k].lazp=0;
}
void build(int k,int l,int r)
{
t[k].l=l;t[k].r=r;t[k].lazp=0;
if(l==r)
{
scanf("%lld",&t[k].sum);
t[k].max=t[k].min=t[k].sum;
return ;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
pushup(k);
}
void add(int k,int l,int r,int v)
{
if(l<=t[k].l&&t[k].r<=r)
{
f(k,v);
return ;
}
pushdown(k);
int mid=(t[k].l+t[k].r)>>1;
if(l<=mid)add(k<<1,l,r,v);
if(r>mid)add(k<<1|1,l,r,v);
pushup(k);
}
void update(int k,int l,int r)
{
if(t[k].max-t[k].min==0&&(l<=t[k].l&&t[k].r<=r))
{
f(k,(ll)sqrt(t[k].max)-t[k].max);
return ;
}
pushdown(k);
int mid=(t[k].l+t[k].r)>>1;
if(l<=mid)update(k<<1,l,r);
if(r>mid)update(k<<1|1,l,r);
pushup(k);
}
ll query(int k,int l,int r)
{
if(l<=t[k].l&&t[k].r<=r)
{
return t[k].sum;
}
ll ans=0;
pushdown(k);
int mid=(t[k].l+t[k].r)>>1;
if(l<=mid)ans+=query(k<<1,l,r);
if(r>mid)ans+=query(k<<1|1,l,r);
return ans;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
build(1,1,n);
int op,l,r,v;
while(m--)
{
scanf("%d",&op);
if(op==2)
{
scanf("%d%d%d",&l,&r,&v);
add(1,l,r,v);
}
else
{
scanf("%d%d",&l,&r);
if(op==1)update(1,l,r);
else printf("%lld\n",query(1,l,r));
}
}
return 0;
}
可持久化数据结构
什么是可持久化数据结构呢?
可持久化数据结构要求在每次进行数据结构的维护后都保存一个历史版本,并且支持对这些历史版本的数据结构进行再操作。
这就使得可持久化数据结构具有O(1)的版本维护和拷贝的特性。
比如O(1)复制整个数据结构,O(1)回退到某个历史版本。
需求:
1、对数组进行下标修改的操作,每次修改后都记录一个版本号ver.
2、对某个版本号的数组进行下标查询的操作。
时间复杂度:瓶颈在于数组拷贝
空间复杂度:瓶颈在于数组拷贝的时候每次都要新开一段内存。
我们可以发现,如果是树形结构,如果它的节点信息是重复的。我们可以把它的节点信息拿过来直接复用。
也就是可以把“树”给压缩成DAG图的形式来节约内存。
实现方式
一种经典的实现方式是先正常建树铺一层“地基”,然后接下来将线段树修改时遍历到的新节点全部新建成新的。
然后对于未遍历到的节点,就直接用孩子指向它们,复用以前的信息
可以存下数据结构所有历史版本,只记录后一个版本与前一个版本不同的地方。
可持久化线段树的老本行,区间k大值查询。
维护一个“二维数组”,第一个维度是数轴,第二个维度是数组的前缀cnt。
256. 最大异或和 - AcWing题库
可持久化字典树
问题分析:
令s[i] = a[1] ^ a[2] ^ … a[i-1] ^ a[i]
则a[p] ^ a[p+1] ^ … ^ a[N] ^ x
相当于 s[N] ^ x ^ s[p-1]
S[N] ^ x
每次可以看成一个固定值 C 提前算出来, 则相当于 求
l
−
1
≤
p
−
1
≤
r
−
1
l-1 \le p-1 \le r-1
l−1≤p−1≤r−1 使得 C 与 s[p-1]异或最大
做法:
s[p-1]的二进制最高位开始到最低位的每一个数字尽量与 C 的二进制对应位相反就可以保证异或出来最大。
s的每个版本可以看成是持久化的trie记录下来。
对持久化trie的每个节点额外维护一个信息version,表示其所属的持久化版本,显然一个节点的version等于其子节点中最大的版本号(因为子节点要么是连向之前的版本,要么创建了该节点后再创建子节点)。 如果一个节点的version 小于
l
−
1
l-1
l−1,说明这个节点是
s
[
l
−
1
]
s[l-1]
s[l−1]插入之前就已经创建出来的节点,不应该考虑在内。
性质:
对持久化树的某一个版本的根节点开始往下访问,所能访问到的节点的版本不会超过该根节点的版本,所以该题只需要从
r
−
1
r-1
r−1版本开始访问即可
注意点:
查询时,节点不空才能转移
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 6e5 + 50, M = N * 25;
int tr[M][2], root[N], s[N], idx;
int max_id[M];
void insert(int i, int k, int p, int q)
{
if(k < 0)
{
max_id[q] = i; // 叶子节点的max_id为自己本身
return;
}
int v = s[i] >> k & 1;
if(p) tr[q][v ^ 1] = tr[p][v ^ 1]; // 上一个结点继承下来的
tr[q][v] = ++idx; // 当前结点新的东西
insert(i, k - 1, tr[p][v], tr[q][v]);
max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]); // 回溯更新父节点max_id
}
int query(int root, int C, int l)
{
int p = root;
for(int i = 23; i >= 0; i--)
{
int v = C >> i & 1;
if(max_id[tr[p][v ^ 1]] >= l) p = tr[p][v ^ 1]; // 满足条件
else p = tr[p][v];
}
return C ^ s[max_id[p]];
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
max_id[0] = -1;
root[0] = ++idx;
insert(0, 23, 0, root[0]);
for(int i = 1; i <= n; i++)
{
int x;
scanf("%d", &x);
root[i] = ++ idx;
s[i] = s[i - 1] ^ x;
insert(i, 23, root[i - 1], root[i]);
}
while(m --)
{
char op[2];
int l, r, x;
scanf("%s", op);
if(*op == 'A')
{
scanf("%d", &x);
n ++;
root[n] = ++ idx;
s[n] = s[n - 1] ^ x;
insert(n, 23, root[n - 1], root[n]);
}
else {
scanf("%d%d%d", &l, &r, &x);
int ans = query(root[r - 1], s[n] ^ x, l - 1);
printf("%d\n", ans);
}
}
return 0;
}
255. 第K小数 - AcWing题库
可持久化线段树。
存储方式
struct
{
int l, r; // 表示在左右子节点的下标
int cnt; // 表示当前区间中,一共有多少个数
}
可持久化线段树难以进行区间修改操作,难以处理懒标记。
思路:
① 离散化
② 在数值上建立线段树,维护每一个区间中一共有多少个数,求整体的第k小数
不加限制的情况下:
离散化之后,每个数都在 [ 0 , n − 1 ] [0,n-1] [0,n−1]之间
如果 [ 0 , m i d ] [0,mid] [0,mid]中, c n t ≥ k cnt \ge k cnt≥k 递归左边 ,第 k k k小数在 [ 0 , m i d ] [0,mid] [0,mid]中。
如果 [ 0 , m i d ] [0,mid] [0,mid]中, c n t < k cnt < k cnt<k递归右边,第 k k k小数在 [ m i d + 1 , r ] [mid + 1, r] [mid+1,r]中。
若 [ 1 , r ] [1,r] [1,r]这个区间:
r o o t [ r ] root[r] root[r]存储的是加入第 r r r个数的信息。
若 [ l , r ] [l, r] [l,r]这个区间:
r o o t [ l − 1 ] root[l-1] root[l−1]存储的是加入到第 l − 1 l-1 l−1个数的信息。
因此 c n t r − c n t l − 1 cnt_r-cnt_{l-1} cntr−cntl−1表示的是 [ l , r ] [l,r] [l,r]区间中数的个数。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int M = 1e4 + 50, N = 1e5 + 50;
int n, m, idx;
int root[N], a[N];
struct Tr{
int l, r;
int cnt;
}tr[N * 4 + N * 17];
vector<int>nums;
int find(int x)
{
return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}
int build(int l, int r)
{
int p = ++idx;
if(l == r) return p;
int mid = (l + r) >> 1;
tr[p].l = build(l, mid), tr[p].r = build(mid + 1, r);
return p;
}
int insert(int p, int l, int r, int x)
{
int q = ++idx;
tr[q] = tr[p];
if(l == r)
{
tr[q].cnt++;
return q;
}
int mid = (l + r) >> 1;
if(x <= mid) tr[q].l = insert(tr[p].l, l, mid, x);
else tr[q].r = insert(tr[p].r, mid + 1, r, x);
tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
return q;
}
int query(int p, int q, int l, int r, int k)
{
if(l == r) return r;
int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
int mid = (l + r) >> 1;
if(cnt >= k) return query(tr[p].l, tr[q].l, l, mid, k);
else return query(tr[p].r, tr[q].r, mid + 1, r, k - cnt);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
nums.push_back(a[i]);
}
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
root[0] = build(0, nums.size() - 1);
for(int i = 1; i <= n; i++)
{
root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i]));
}
while(m --)
{
int l, r, k;
scanf("%d%d%d", &l, &r, &k);
printf("%d\n", nums[query(root[l - 1], root[r], 0, nums.size() - 1, k)]);
}
return 0;
}
树的DFS序
对于一颗二叉树,它有三种遍历方式:
前序
中序
后序
这三种顺序其实都有使用的场景(主要是在分治算法中考虑是递归前,递归进行中,递归后)的区别。
在多叉树里面,常用的dfs序其实也有三种。
dfs序
扩展dfs序
欧拉序
dfs序类似二叉树的先序遍历
扩展dfs序类似二叉树的先序+中序
欧拉序类似二叉树的先序+后序
在树上问题中dfs序列往往是为了构建“树”与“序列”的桥梁。
绝大部分的“子树”操作类问题,“树链”操作问题,其实本质来讲都是利用了dfs序列将树结构整理成数组。
定义多叉树的dfs序列是一种类二叉树先序遍历。
定义dfn是dfs遍历的标号计数变量。
定义id[x](L)数组是x节点在遍历的过程中被标记的dfn的值。
定义idx(mp)[x]数组是dfn为x的节点是哪个节点。
对于一颗多叉树,发现将其利用dfs序重标号后,每一颗子树的dfs序全都是连续的。
之前定义过id数组,其实它还有一个含义,它还表示L数组。
定义L[x]数组表示在dfs遍历的过程中x所覆盖子树的dfn最小值。
定义R[x]数组表示在dfs遍历的过程中x所覆盖子树的dfn最大值。
有了LR数组,子树的维护问题就转化成了数组区间的维护问题
扩展dfs序
待学习
欧拉序
欧拉序是一种“类前后序”,原本我们处理LR数组的时候,一般来讲只对L进行dfn++的操作,换句话说就是其实并没有为回溯的时候分配dfn。
一般认为只有分配了dfn才能够称得上是一次“遍历”不然只不过是在函数调用过程中路过了一下。
树上差分
一般来讲,树上差分或者树上前缀和是为了解决静态的子树加数求和问题,在步骤上可以不用求LR那些,可以直接做。
树上倍增
树上倍增的写法类似ST表,但是不论维护哪些信息,都要首先维护一个fa数组, f a [ i ] [ j ] fa[i][j] fa[i][j]表示树上的第i个节点向上跳跃 2 j 2^j 2j后的父节点是谁。
预处理的话其实就 f a [ i ] [ j ] = f a [ f a [ i ] [ j − 1 ] ] [ j − 1 ] fa[i][j]=fa[fa[i][j-1]][j-1] fa[i][j]=fa[fa[i][j−1]][j−1]。很好理解。
使用起来就是位运算的二分的写法就行。