树状数组
241. 楼兰图腾
在完成了分配任务之后,西部 314 来到了楼兰古城的西部。
相传很久以前这片土地上(比楼兰古城还早)生活着两个部落,一个部落崇拜尖刀(V),一个部落崇拜铁锹(∧),他们分别用 V 和 ∧ 的形状来代表各自部落的图腾。
西部 314 在楼兰古城的下面发现了一幅巨大的壁画,壁画上被标记出了 n 个点,经测量发现这 n 个点的水平位置和竖直位置是两两不同的。
西部 314 认为这幅壁画所包含的信息与这 n 个点的相对位置有关,因此不妨设坐标分别为 (1,y1),(2,y2),…,(n,yn),其中 y1∼yn 是 1 到 n 的一个排列。
西部 314 打算研究这幅壁画中包含着多少个图腾。
如果三个点 (i,yi),(j,yj),(k,yk) 满足 1≤i
西部314 想知道,这 n 个点中两个部落图腾的数目。
因此,你需要编写一个程序来求出 V 的个数和 ∧ 的个数。
输入格式
第一行一个数 n。
第二行是 n 个数,分别代表 y1,y2,…,yn。
输出格式
两个数,中间用空格隔开,依次为 V 的个数和 ∧ 的个数。
数据范围
对于所有数据,n≤200000,且输出答案不会超过 int64。
y1∼yn 是 1 到 n 的一个排列。
输入样例:
5
1 5 3 2 4
输出样例:
3 4

#include <iostream>#include <cstdio>#include <cstring>using namespace std;const int N = 2000010;typedef long long LL;int n;//t[i]表示树状数组i结点覆盖的范围和int a[N], t[N];//Lower[i]表示左边比第i个位置小的数的个数//Greater[i]表示左边比第i个位置大的数的个数int Lower[N], Greater[N];//返回非负整数x在二进制表示下最低位1及其后面的0构成的数值int lowbit(int x){return x & -x;}//将序列中第x个数加上k。void add(int x, int k){for(int i = x; i <= n; i += lowbit(i)) t[i] += k;}//查询序列前x个数的和int ask(int x){int sum = 0;for(int i = x; i; i -= lowbit(i)) sum += t[i];return sum;}int main(){scanf("%d", &n);for(int i = 1; i <= n; i++) scanf("%d", &a[i]);//从左向右,依次统计每个位置左边比第i个数y小的数的个数、以及大的数的个数for(int i = 1; i <= n; i++){int y = a[i]; //第i个数//在前面已加入树状数组的所有数中统计在区间[1, y - 1]的数字的出现次数Lower[i] = ask(y - 1);//在前面已加入树状数组的所有数中统计在区间[y + 1, n]的数字的出现次数Greater[i] = ask(n) - ask(y);//将y加入树状数组,即数字y出现1次add(y, 1);}//清空树状数组,从右往左统计每个位置右边比第i个数y小的数的个数、以及大的数的个数memset(t, 0, sizeof t);LL resA = 0, resV = 0;//从右往左统计for(int i = n; i >= 1; i--){int y = a[i];resA += (LL)Lower[i] * ask(y - 1);resV += (LL)Greater[i] * (ask(n) - ask(y));//将y加入树状数组,即数字y出现1次add(y, 1);}printf("%lld %lld\n", resV, resA);return 0;}
#include <iostream>#include <cstring>#include <algorithm>typedef long long LL;using namespace std;const int N = 2e5 + 10;int a[N], tr[N];int n;LL Greate[N], Lower[N];int lowbit(int x) {return x & -x;}void update(int x, int c) // 位置x加c{for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;}int query(int x) // 返回前x个数的和{int res = 0;for (int i = x; i; i -= lowbit(i)) res += tr[i];return res;}int main() {cin >> n;for (int i = 1; i <= n; i++) {cin >> a[i];}for (int i = 1; i <= n; i++) {int y = a[i];Greate[i] = query(n) - query(y);Lower[i] = query(y - 1);update(y, 1);}memset(tr, 0, sizeof tr);LL resA = 0, resV = 0;for (int i = n; i >= 1; i--) {int y = a[i];resA += Lower[i] * query(y - 1);resV += Greate[i] * (query(n) - query(y));update(y, 1);}cout << resV << " " << resA << endl;}
242. 一个简单的整数问题
给定长度为 N 的数列 A,然后输入 M 行操作指令。
第一类指令形如 C l r d,表示把数列中第 l∼r 个数都加 d。
第二类指令形如 Q x,表示询问数列中第 x 个数的值。
对于每个询问,输出一个整数表示答案。
输入格式
第一行包含两个整数 N 和 M。
第二行含 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤10^5,
|d|≤10000,
|A[i]|≤10^9
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2
输出样例:
4
1
2
5

#include <iostream>#include <cstring>#include <algorithm>typedef long long LL;using namespace std;const int N = 1e5 + 10;int a[N], n, m;int tr[N];int lowbit(int x) {return x & -x;}void update(int x, int c) // 位置x加c{for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;}LL query(int x) // 返回前x个数的和{LL res = 0;for (int i = x; i; i -= lowbit(i)) res += tr[i];return res;}int main() {cin >> n >> m;for (int i = 1; i <= n; i++) {cin >> a[i];}for (int i = 1; i <= n; i++) {update(i, a[i] - a[i - 1]); // 存入差分序列}while (m--) {char op[2];int l, r, d, x;scanf("%s", &op);if (op[0] == 'C') {scanf("%d%d%d", &l, &r, &d);update(l, d); // 差分修改update(r + 1, -d);} else {scanf("%d", &x);cout << query(x) << endl;}}}
244. 谜一样的牛
有 n 头奶牛,已知它们的身高为 1∼n 且各不相同,但不知道每头奶牛的具体身高。
现在这 n 头奶牛站成一列,已知第 i 头牛前面有 Ai 头牛比它低,求每头奶牛的身高。
输入格式
第 1 行:输入整数 n。
第 2..n 行:每行输入一个整数 Ai,第 i 行表示第 i 头牛前面有 Ai 头牛比它低。
(注意:因为第 1 头牛前面没有牛,所以并没有将它列出)
输出格式
输出包含 n 行,每行输出一个整数表示牛的身高。
第 i 行输出第 i 头牛的身高。
数据范围
1≤n≤10^5
输入样例:
5
1
2
1
0
输出样例:
2
4
5
3
1
#include <iostream>#include <cstring>#include <algorithm>using namespace std;const int N = 1e5 + 10;int h[N], n;int tr[N];int ans[N];int lowbit(int x) {return x & -x;}void update(int x, int c) // 位置x加c{for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;}int query(int x) // 返回前x个数的和{int res = 0;for (int i = x; i; i -= lowbit(i)) res += tr[i];return res;}int main() {cin >> n;for (int i = 2; i <= n; i++) {cin >> h[i];}for (int i = 1; i <= n; i++) {update(i, 1); //每个位置插入1}for (int i = n; i; i--) {int k = h[i] + 1;int l = 1, r = n;while (l < r) { // 在没有用过的数当中 二分找第k个数int mid = l + r >> 1;if (query(mid) >= k) {r = mid;} else {l = mid + 1;}}ans[i] = l;update(l, -1); // l这个位置的数已经用过,update(l,-1)相当于将这个位置为标记为用过}for (int i = 1; i <= n; i++) {cout << ans[i] << endl;}}
4316. 合适数对
给定一个长度为 n 的整数数列 a1,a2,…,an 和一个整数 t。
请你判断共有多少个数对 (l,r) 同时满足:
1≤l≤r≤n
al+al+1+…+ar−1+ar
第一行包含两个整数 n 和 t。
第二行包含 n 个整数 a1,a2,…,an。
输出格式
一个整数,表示满足条件的数对的数量。
数据范围
前三个测试点满足 1≤n≤5。
所有测试点满足 1≤n≤2×10^5,|t|≤2×1014,|ai|≤109。
输入样例1:
5 4
5 -1 3 4 -1
输出样例1:
5
输入样例2:
3 0
-1 2 -3
输出样例2:
4
输入样例3:
4 -1
-2 1 -2 3
输出样例3:
3
// 暴力解法,直接前缀和#include <iostream>#include <cstring>#include <algorithm>using namespace std;typedef long long LL;int n,m;const int N = 2e5+10;LL a[N],s[N];int res;int main(){cin >> n >> m;for (int i = 1; i <= n; i ++ ){cin >> a[i];s[i] = s[i-1]+a[i];}for (int i = 1; i <= n; i ++ ){for (int j = i; j <= n; j ++ ){if(s[j]-s[i-1]<m){res++;}}}cout << res;}
// 树状数组#include <iostream>#include <cstring>#include <algorithm>#include <vector>using namespace std;typedef long long LL;const int N = 2e6 + 10, M = N * 2;LL n, m;LL s[N],cnt;LL tr[M];vector<LL> nums;int lowbit(int x) {return x & -x;}void add(int x, int k) {for (int i = x; i <= cnt; i += lowbit(i)) tr[i] += k;}int sum(int x) {int res = 0;for (int i = x; i; i -= lowbit(i)) res += tr[i];return res;}int get(LL x) {return lower_bound(nums.begin(), nums.end(), x) - nums.begin() + 1;}int main() {cin >> n >> m;for (int i = 1; i <= n; i++) {cin >> s[i];s[i] += s[i - 1];}for (int i = 0; i <= n; i++) { //将所有前缀和以及后续询问用到的数值全部离散化nums.push_back(s[i]);nums.push_back(s[i] - m);}sort(nums.begin(), nums.end());nums.erase(unique(nums.begin(), nums.end()), nums.end());// 把每一个数 添加到树状数组中LL res = 0;cnt = nums.size();add(get(s[0]), 1);for (int i = 1; i <= n; i++) {res += sum(cnt) - sum(get(s[i] - m));add(get(s[i]), 1);}cout << res;}
线段树
1275. 最大数
给定一个正整数数列 a1,a2,…,an,每一个数都在 0∼p−1 之间。
可以对这列数进行两种操作:
添加操作:向序列后添加一个数,序列长度变成 n+1;
询问操作:询问这个序列中最后 L 个数中最大的数是多少。
程序运行的最开始,整数序列为空。
一共要对整数序列进行 m 次操作。
写一个程序,读入操作的序列,并输出询问操作的答案。
输入格式
第一行有两个正整数 m,p,意义如题目描述;
接下来 m 行,每一行表示一个操作。
如果该行的内容是 Q L,则表示这个操作是询问序列中最后 L 个数的最大数是多少;
如果是 A t,则表示向序列后面加一个数,加入的数是 (t+a) mod p。其中,t 是输入的参数,a 是在这个添加操作之前最后一个询问操作的答案(如果之前没有询问操作,则 a=0)。
第一个操作一定是添加操作。对于询问操作,L>0 且不超过当前序列的长度。
输出格式
对于每一个询问操作,输出一行。该行只有一个数,即序列中最后 L 个数的最大数。
输入样例:
10 100
A 97
Q 1
Q 1
A 17
Q 2
A 63
Q 1
Q 1
Q 3
A 99
输出样例:
97
97
97
60
60
97
样例解释
最后的序列是 97,14,60,96。
#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>using namespace std;typedef long long LL;const int N = 200010;int m, p;struct Node{int l, r;int v; // 区间[l, r]中的最大值}tr[N * 4];void pushup(int u) // 由子节点的信息,来计算父节点的信息{tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);}void build(int u, int l, int r){tr[u] = {l, r};if (l == r) return;int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);}int query(int u, int l, int r){if (tr[u].l >= l && tr[u].r <= r) return tr[u].v; // 树中节点,已经被完全包含在[l, r]中了int mid = tr[u].l + tr[u].r >> 1;int v = 0;if (l <= mid) v = query(u << 1, l, r);if (r > mid) v = max(v, query(u << 1 | 1, l, r));return v;}void modify(int u, int x, int v){if (tr[u].l == x && tr[u].r == x) tr[u].v = v;else{int mid = tr[u].l + tr[u].r >> 1;if (x <= mid) modify(u << 1, x, v);else modify(u << 1 | 1, x, v);pushup(u);}}int main(){int n = 0, last = 0;scanf("%d%d", &m, &p);build(1, 1, m);int x;char op[2];while (m -- ){scanf("%s%d", op, &x);if (*op == 'Q'){last = query(1, n - x + 1, n);printf("%d\n", last);}else{modify(1, n + 1, ((LL)last + x) % p);n ++ ;}}return 0;}
245. 你能回答这些问题吗
给定长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
1 x y,查询区间 [x,y] 中的最大连续子段和,即 maxx≤l≤r≤y{∑i=lrA[i]}。
2 x y,把 A[x] 改成 y。
对于每个查询指令,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M 行每行 3 个整数 k,x,y,k=1 表示查询(此时如果 x>y,请交换 x,y),k=2 表示修改。
输出格式
对于每个查询指令输出一个整数表示答案。
每个答案占一行。
输入样例:
5 3
1 2 -3 4 5
1 2 3
2 2 -1
1 3 2
输出样例:
2
-1
#include <iostream>#include <cstring>#include <algorithm>using namespace std;const int N = 5e5+10;int w[N];struct Node{int l, r;int sum; // a[l]...a[r]的和int lmax,rmax,tmax; // 以l为起点的最大连续子段和的长度}tr[N * 4];/*分治法求最大连续子段和*/void pushup(Node &u,Node &l,Node &r){u.sum = l.sum + r.sum;u.lmax = max(l.lmax,l.sum+r.lmax);u.rmax = max(r.rmax,r.sum + l.rmax);u.tmax = max(max(l.tmax,r.tmax),l.rmax + r.lmax);}void pushup(int u){pushup(tr[u],tr[u<<1],tr[u<<1|1]); // 这里要写|不要写成+}void build(int u, int l, int r){if (l == r) tr[u] = {l, r,w[l],w[l],w[l],w[l]};else{tr[u] = {l, r};int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);pushup(u);}}void modify(int u, int x, int v){ // 将x节点修改为vif(tr[u].l==x&&tr[u].r==x){tr[u] = {x,x,v,v,v,v};return;}int mid = tr[u].l+tr[u].r >> 1;if(x<=mid){modify(u<<1,x,v);}else{modify(u<<1|1,x,v);}pushup(u);}Node query(int u, int l, int r){if (tr[u].l >= l && tr[u].r <= r){return tr[u];}else{int mid = tr[u].l + tr[u].r >> 1;if(r<=mid){return query(u<<1,l,r);}else if(l>mid){return query(u<<1|1,l,r);}else{Node res;auto t1 = query(u<<1,l,r);auto t2 = query(u<<1|1,l,r);pushup(res,t1,t2);return res;}}}int main(){int n,m;cin >> n >> m;for (int i = 1; i <= n; i ++ ){cin >> w[i];}build(1,1,n);while (m -- ){int t,x,y;cin >> t >> x >> y;if(t==1){if(x>y){swap(x,y);}auto t = query(1,x,y);cout << t.tmax <<endl;}else{modify(1,x,y);}}}
246. 区间最大公约数
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 d。
Q l r,表示询问 A[l],A[l+1],…,A[r] 的最大公约数(GCD)。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
#include <iostream>#include <cstring>#include <algorithm>typedef long long LL;const int N = 5e5+10;using namespace std;LL w[N];int n,m;LL gcd(LL a, LL b) // 欧几里得算法{return b ? gcd(b, a % b) : a;}struct Node{int l, r;LL sum; // l,r区间的和LL d; // l,r区间的最大公约数}tr[N * 4];void pushup(Node &u, Node &l, Node &r){u.sum = l.sum+r.sum;u.d = gcd(l.d,r.d);}void pushup(int u){pushup(tr[u],tr[u<<1],tr[u<<1|1]);}void build(int u, int l, int r){if (l == r) {LL b = w[l]-w[l-1]; // 这里关键tr[u] = {l, r, b, b};}else{tr[u] = {l, r};int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);pushup(u);}}Node query(int u, int l, int r){if (tr[u].l >= l && tr[u].r <= r){return tr[u];}else{int mid = tr[u].l + tr[u].r >> 1;if(r<=mid){return query(u<<1,l,r);}else if(l>mid){return query(u<<1|1,l,r);}else{Node res,t1,t2;t1 = query(u<<1, l, r);t2 = query(u<<1|1, l, r);pushup(res, t1, t2);return res;}}}void modify(int u,int x,LL v){if(tr[u].l==x&&tr[u].r==x){LL b = tr[u].sum + v;tr[u] = {x,x,b,b};}else{int mid = tr[u].l + tr[u].r >>1;if(x<=mid){modify(u<<1,x,v);}else{modify(u<<1|1,x,v);}pushup(u);}}int main(){cin >> n >> m;for (int i = 1; i <= n; i ++ ){cin >> w[i];}build(1,1,n);char op[5];int l,r;LL d;while (m -- ){scanf("%s", &op);if(op[0]=='C'){cin >> l >> r >>d;modify(1,l,d);if(r+1<=n){modify(1,r+1,-d);}}else{cin >> l >> r;Node t1={0,0,0,0};auto t2 = query(1,1,l);if(l+1<=r){t1 = query(1,l+1,r);}/* 这里为什么要求一个区间[1,l]的区间和呢,看下面解释*/cout << abs(gcd(t2.sum,t1.d))<<endl;}}}/*差分序列原数列 w[1] w[2] w[3] w[4] w[5]差分数列w[1]-w[0] w[2]-w[1] w[3]-w[2] w[4]-w[3] w[5]-w[4]差分数列b[1] b[2] b[3] b[4] b[5]假如现在目标是求[2,4]区间的最大公约数那么不能直接查询query(1,2,4)的公约数,因为 差分序列b[2]不仅包含了w[2],还包含了w[1]所以就求从1到l的前缀和,得出w[2]的值*/
243. 一个简单的整数问题2
给定一个长度为 N 的数列 A,以及 M 条指令,每条指令可能是以下两种之一:
C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 d。
Q l r,表示询问数列中第 l∼r 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数 N,M。
第二行 N 个整数 A[i]。
接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4
输出样例:
4
55
9
15
#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>using namespace std;typedef long long LL;const int N = 100010;int n, m;int w[N];struct Node{int l, r;LL sum, add;}tr[N * 4];void pushup(int u){tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;}void pushdown(int u){auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];if (root.add){left.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;root.add = 0;}}void build(int u, int l, int r){if (l == r) tr[u] = {l, r, w[r], 0};else{tr[u] = {l, r};int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);pushup(u);}}void modify(int u, int l, int r, int d){if (tr[u].l >= l && tr[u].r <= r){tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * d;tr[u].add += d; // 这里懒标记了,等下次用到这个点的时候再更新就行// 那么什么时候用到呢/*1. 再次修改,可能会涉及到这个区间,所以下面的else那块有一样pushdown2. 查询时涉及这个区间,那么一定得先pushdown*/}else // 一定要分裂{pushdown(u);int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u << 1, l, r, d);if (r > mid) modify(u << 1 | 1, l, r, d);pushup(u);}}LL query(int u, int l, int r){if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;pushdown(u);int mid = tr[u].l + tr[u].r >> 1;LL sum = 0;if (l <= mid) sum = query(u << 1, l, r);if (r > mid) sum += query(u << 1 | 1, l, r);return sum;}int main(){scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);build(1, 1, n);char op[2];int l, r, d;while (m -- ){scanf("%s%d%d", op, &l, &r);if (*op == 'C'){scanf("%d", &d);modify(1, l, r, d);}else printf("%lld\n", query(1, l, r));}return 0;}
1277. 维护序列
老师交给小可可一个维护数列的任务,现在小可可希望你来帮他完成。
有长为 N 的数列,不妨设为 a1,a2,…,aN。
有如下三种操作形式:
把数列中的一段数全部乘一个值;
把数列中的一段数全部加一个值;
询问数列中的一段数的和,由于答案可能很大,你只需输出这个数模 P 的值。
输入格式
第一行两个整数 N 和 P;
第二行含有 N 个非负整数,从左到右依次为 a1,a2,…,aN;
第三行有一个整数 M,表示操作总数;
从第四行开始每行描述一个操作,输入的操作有以下三种形式:
操作 1:1 t g c,表示把所有满足 t≤i≤g 的 ai 改为 ai×c;
操作 2:2 t g c,表示把所有满足 t≤i≤g 的 ai 改为 ai+c;
操作 3:3 t g,询问所有满足 t≤i≤g 的 ai 的和模 P 的值。
同一行相邻两数之间用一个空格隔开,每行开头和末尾没有多余空格。
输出格式
对每个操作 3,按照它在输入中出现的顺序,依次输出一行一个整数表示询问结果。
输入样例:
7 43
1 2 3 4 5 6 7
5
1 2 5 5
3 2 4
2 3 7 9
3 1 3
3 4 7
输出样例:
2
35
8
样例解释
初始时数列为 {1,2,3,4,5,6,7};
经过第 1 次操作后,数列为 {1,10,15,20,25,6,7};
对第 2 次操作,和为 10+15+20=45,模 43 的结果是 2;
经过第 3 次操作后,数列为 {1,10,24,29,34,15,16};
对第 4 次操作,和为 1+10+24=35,模 43 的结果是 35;
对第 5 次操作,和为 29+34+15+16=94,模 43 的结果是 8。
#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>using namespace std;typedef long long LL;const int N = 100010;int n, m,p;int w[N];struct Node{int l, r;LL sum, add, mul;}tr[N * 4];void pushup(int u){tr[u].sum = (tr[u << 1].sum + tr[u << 1 | 1].sum)%p; // 这里取模}void eval(Node& t,LL add,LL mul){t.sum = (t.sum*mul)%p + (((LL)t.r - t.l + 1)*add) % p; // 取模t.mul = (t.mul*mul)%p;t.add = (t.add * mul + add)%p;}void pushdown(int u){auto &t = tr[u];eval(tr[u<<1],t.add,t.mul);eval(tr[u<<1|1],t.add,t.mul);t.add = 0;t.mul = 1;}void build(int u, int l, int r){if (l == r) tr[u] = {l, r, w[r], 0, 1 }; // 这里要改else{tr[u] = {l, r,0,0,1}; // 这里要改int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);pushup(u);}}void modify(int u, int l, int r, LL add, LL mul){if (tr[u].l >= l && tr[u].r <= r){eval(tr[u], add, mul);}else // 一定要分裂{pushdown(u);int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u << 1, l, r, add,mul);if (r > mid) modify(u << 1 | 1, l, r, add,mul);pushup(u);}}LL query(int u, int l, int r){if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;pushdown(u);int mid = tr[u].l + tr[u].r >> 1;LL sum = 0;if (l <= mid) sum = query(u << 1, l, r)%p;if (r > mid) {sum = (sum + query(u << 1 | 1, l, r))%p;}return sum;}int main(){scanf("%d%d", &n,&p);for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);cin >> m;build(1, 1, n);int op;int l, r;LL d;while (m -- ){scanf("%d%d%d", &op, &l, &r);if (op == 1){scanf("%d", &d);modify(1, l, r, 0,d);}else if(op==2){scanf("%d", &d);modify(1, l, r, d,1);}else printf("%lld\n", query(1, l, r)%p);}return 0;}
247. 亚特兰蒂斯
有几个古希腊书籍中包含了对传说中的亚特兰蒂斯岛的描述。
其中一些甚至包括岛屿部分地图。
但不幸的是,这些地图描述了亚特兰蒂斯的不同区域。
您的朋友 Bill 必须知道地图的总面积。
你自告奋勇写了一个计算这个总面积的程序。
输入格式
输入包含多组测试用例。
对于每组测试用例,第一行包含整数 n,表示总的地图数量。
接下来 n 行,描绘了每张地图,每行包含四个数字 x1,y1,x2,y2(不一定是整数),(x1,y1) 和 (x2,y2) 分别是地图的左上角位置和右下角位置。
注意,坐标轴 x 轴从上向下延伸,y 轴从左向右延伸。
当输入用例 n=0 时,表示输入终止,该用例无需处理。
输出格式
每组测试用例输出两行。
第一行输出 Test case #k,其中 k 是测试用例的编号,从 1 开始。
第二行输出 Total explored area: a,其中 a 是总地图面积(即此测试用例中所有矩形的面积并,注意如果一片区域被多个地图包含,则在计算总面积时只计算一次),精确到小数点后两位数。
在每个测试用例后输出一个空行。
输入样例:
2
10 10 20 20
15 15 25 25.5
0
输出样例:
Test case #1
Total explored area: 180.00
样例解释
样例所示地图覆盖区域如下图所示,两个矩形区域所覆盖的总面积,即为样例的解。



建立了线段树, 我们再考虑实际线段要怎么存进去, 假设我们存储的是点. 那么当前最小节点是类似[1, 1], [2, 2]这样,
显然他们单独的长度都是0(因为是点)
假设离散化后的ys为 [5.1, 6.2, 8.5, 10.8, …]
考虑线段树内[1, 1], [2, 2]的父节点[1, 2], 我们若需要[1, 2]这个节点的len能够
映射离散化区间里面6.2到8.5的距离的话, 即需要tr[x].r找到8.5, tr[x].l找到6.2 这样看着是可行的
但是这样的话, 当线段树被切分成单位点的时候, 比如若线段树根节点是[0, 2]
要查询[1,2]的长度, 按照线段树国际惯例, 我们下一步会切成[0, 1]和[2, 2], 那么可以发现, 这里我们弄断了两点之间的线段, 且很难继续记录他们之间的关联, 导致难以进行下去
所以, 我们换一种映射方法, 我们考虑令线段树的最小单位是映射相邻两点之间的线段:
同样假设离散化后的ys为 [5.1, 6.2, 8.5, 10.8, …]
考虑线段树区间[0, 0], 我们需要它映射到5.1~6.2这个区间, 同理[1, 1]映射[6.2, 8.5] …
那么, 我们每个线段树最小单位映射的长度, 就应该是ys[tr[t].r + 1] - ys[tr[t].l] 即可
很容易看出, 该做法可以推到更上层的节点也不会出错
#include <cstdio>#include <cstring>#include <algorithm>#include <vector>using namespace std;const int N = 10010;int n;// 用于存每一组数据里面的每一条竖线(线段)struct Segment{double x, y1, y2;int k; //标记左右竖线bool operator<(const Segment &t)const{return x < t.x;}}seg[N * 2]; // 每个测试用例包括n组数据(n<=10000), 每组数组是1个矩形, 存2条竖线struct Node{int l, r; // 左右端点int cnt; // 当前区段(整段)被有效覆盖次数double len; // 当前区间内的有效长度}tr [N * 8]; // 每个测试用例最多存在, 2n个y坐标再 * 4(线段树)vector<double> ys;int find(double y){return lower_bound(ys.begin(), ys.end(), y) - ys.begin();}void pushup(int u){// 如果该"整段"需要算入lenif (tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];// 如果tr[u].cnt等于0其实有两种情况:// 1. 完全覆盖. 这种情况由modify的第一个if进入.// 这时下面其实等价于把"由完整的l, r段贡献给len的部分清除掉",// 而留下其他可能存在的子区间段对len的贡献// 2. 不完全覆盖, 这种情况由modify的else最后一行进入.// 表示区间并不是完全被覆盖,可能有部分被覆盖,所以要通过儿子的信息来更新else if (tr[u].cnt == 0 && tr[u].l != tr[u].r) //加个==0清晰点{tr[u].len = tr[u<<1].len + tr[u<<1|1].len;}else tr[u].len = 0; // 不可再分且cnt等于0, 那就直接安全置0}void build(int u, int l, int r){if (l == r) tr[u] = {l, r, 0, 0};if (l != r){tr[u] = {l, r};int mid = l + r >> 1;build(u<<1, l, mid), build(u<<1|1, mid+1, r);pushup(u);}}void modify(int u, int l, int r, int k){// 线段被包住, 说明整体都是要改的if (tr[u].l >= l && tr[u].r <= r){tr[u].cnt += k; // cnt是当前还在计算面积的矩形个数pushup(u);}else{int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u<<1, l, r, k);if (r > mid) modify(u<<1|1, l, r, k);pushup(u);}}// debug用void print_tree(){puts("tree:");for (int i = 0; i <= ys.size()-2; i++){printf("i: %d, l: %d r: %d cnt: %d, len: %lf\n",i, tr[i].l, tr[i].r, tr[i].cnt, tr[i].len);}}int main(){int T = 1;// 测试用例组数不定, 直到n为0才停止while (scanf("%d", &n), n){ys.clear();// 每组测试用例有n个矩形输入for (int i = 0, j = 0; i < n; i++){double x1, y1, x2, y2;scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);seg[j++] = {x1, y1, y2, 1}; //左竖线seg[j++] = {x2, y1, y2, -1}; //右竖线ys.push_back(y1), ys.push_back(y2); // 离散化压缩空间}// 离散化sort(ys.begin(), ys.end());ys.erase(unique(ys.begin(), ys.end()), ys.end());// 对该组所有测试用例的y坐标离散后所在区间构造线段树// y轴有ys.size()个点, 而我们线段树需要存线段, 这里一共有ys.size()-1个线段// 而线段树build是每个单位点为最小单位, 所以最少得build ys.size()-1个点// 所以r最小是build(1, 0, ys.size()-2) 实际上r只需要不比这个ys.size()-2小就行build(1, 0, ys.size() - 2);// 建立了线段树, 我们再考虑实际线段要怎么存进去, 当前最小节点是类似[1, 1], [2, 2]这样,// 显然他们单独的长度都是0(因为是点)// 假设离散化后的ys为 [5.1, 6.2, 8.5, 10.8, ...]// 考虑线段树内[1, 1], [2, 2]的父节点[1, 2], 我们若需要[1, 2]这个节点的len能够// 映射离散化区间里面6.2到8.5的距离的话, 即需要tr[x].r找到8.5, tr[x].l找到6.2// 这样看着是可行的// 但是这样的话, 当线段树被切分成单位点的时候, 比如若线段树根节点是[0, 2]// 要查询[1,2]的长度, 按照线段树国际惯例, 我们下一步会切成[0, 1]和[2, 2], 那么可以发现,// 这里我们弄断了两点之间的线段, 且很难继续记录他们之间的关联, 导致难以进行下去// 所以, 我们换一种映射方法, 我们考虑令线段树的最小单位是映射相邻两点之间的线段:// 同样假设离散化后的ys为 [5.1, 6.2, 8.5, 10.8, ...]// 考虑线段树区间[0, 0], 我们需要它映射到5.1~6.2这个区间, 同理[1, 1]映射[6.2, 8.5] ...// 那么, 我们每个线段树最小单位映射的长度, 就应该是ys[tr[t].r + 1] - ys[tr[t].l] 即可// 很容易看出, 该做法可以推到更上层的节点也不会出错// 根据x的大小排序, 确定后面遍历是正确的从左到右sort(seg, seg + n * 2);double res = 0;for (int i = 0; i < n * 2; i++){// tr[1].len是当前y轴实际使用长度, 乘上x就是面积if (i > 0)res += tr[1].len * (seg[i].x - seg[i-1].x);// 线段树插入当前y轴上的用到的线段, .k是标记左右竖线// 参考上面调用build之前的注释, 从tr去取出真实ys坐标(y轴离散化后)要+1// 那么从真实ys坐标找回对应tr内的右点就减回去1modify(1, find(seg[i].y1), find(seg[i].y2) - 1, seg[i].k);}printf("Test case #%d\n", T++);printf("Total explored area: %.2lf\n\n", res);}return 0;}
可持久化数据结构
最大异或和
给定一个非负整数序列 a,初始长度为 N。
有 M 个操作,有以下两种操作类型:
A x:添加操作,表示在序列末尾添加一个数 x,序列的长度 N 增大 1。
Q l r x:询问操作,你需要找到一个位置 p,满足 l≤p≤r,使得:a[p] xor a[p+1] xor … xor a[N] xor x 最大,输出这个最大值。
输入格式
第一行包含两个整数 N,M,含义如问题描述所示。
第二行包含 N 个非负整数,表示初始的序列 A。
接下来 M 行,每行描述一个操作,格式如题面所述。
输出格式
每个询问操作输出一个整数,表示询问的答案。
每个答案占一行。
输入样例:
5 5
2 6 4 3 6
A 1
Q 3 5 4
A 4
Q 5 7 0
Q 3 6 6
输出样例:
4
5
6
#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>using namespace std;const int N = 600010, M = N * 25;int n, m;int s[N];int tr[M][2], max_id[M];int root[N], idx;/*i是s[i]中的i,即s的下标k是当前处理到第几位p是上一个版本q是最新版本*/void insert(int i, int k, int p, int q){if (k < 0){max_id[q] = i;return;}int v = s[i] >> k & 1;// 当前这位是v可以将原先的覆盖掉// 所以只需要将v^1 这一位从p这个版本复制过来if (p) tr[q][v ^ 1] = tr[p][v ^ 1];// 当前的点必须开一个新的点tr[q][v] = ++ idx;// 递归插入另一个insert(i, k - 1, tr[p][v], tr[q][v]);// 更新一下当前版本的最大idmax_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);}/*本题目是在区间[l,r]中寻找一个p使得a[p]^a[p+1].....^a[N] ^ x值最大那考虑到b^a^a = b所以我们预处理一个前缀和s[1] = a[1]s[2] = a[1]^a[2]s[3] = a[1]^a[2]^a[3]....s[n] = a[1]^a[2]^a[3]....^a[n]要 求a[p]^a[p+1].....^a[N]的值,只需要s[n]^s[p-1]即可*//*root 根节点C 从root为根的子树中找与C异或最大的数因此题目中的p是从[l,r]中找那么p-1就是从[l-1,r-1]中找*/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(){scanf("%d%d", &n, &m);/*这里为什么必须将max_id[0]设置成一个非法值?注意到在查询函数中if (max_id[tr[p][v ^ 1]] >= L) p = tr[p][v ^ 1];如果tr[p][v^1]这个点不存在,那么他的值肯定是0,那就相当于if (max_id[0] >= L) p = tr[p][v ^ 1];当L = 0时,本来是不存在这个点(tr[p][v ^ 1])的却要走向这个点,那么走向这个点就一定会将结果变小假设前面已经匹配一段了abcde,待匹配是xxxxabcdexxxx但是如果走向这个不存在的点的话query函数最后返回那块C ^ s[max_id[p]];s[max_id[p]] = s[0]=0,而正确结果至少是匹配abcde这一段大于等于0*/max_id[0] = -1;/*为什么又要初始化root[0]并且插入0节点呢因为p-1的范围是[l-1,r-1],当l-1==0时,意味着有可能和0匹配,所以必须加*/root[0] = ++ idx;insert(0, 23, 0, root[0]);for (int i = 1; i <= n; i ++ ){int x;scanf("%d", &x);s[i] = s[i - 1] ^ x;root[i] = ++ idx;insert(i, 23, root[i - 1], root[i]);}char op[2];int l, r, x;while (m -- ){scanf("%s", op);if (*op == 'A'){scanf("%d", &x);n ++ ;s[n] = s[n - 1] ^ x;root[n] = ++ idx;insert(n, 23, root[n - 1], root[n]);}else{scanf("%d%d%d", &l, &r, &x);printf("%d\n", query(root[r - 1], s[n] ^ x, l - 1));}}return 0;}
#include <cstdio>#include <cstring>#include <algorithm>using namespace std;// 30w初始数据和30w新增, 而10的7次方小于2的24次方, 再加上根节点, 就是说每个数最多需要25位;const int N = 600010, M = N * 25;int n, m;int s[N]; // 前缀和序列int tr[M][2];int max_id[M]; // 用于记录当前根节点版本的最大id范围int root[N], idx;// i是第i个插入的数的i, p是上一个插入的数的节点号, q是当前节点号, k是现在取到第k位void insert(int i, int k, int p, int q){// 如果记录结束了if (k < 0){max_id[q] = i; // 记录当前节点(可能会被后面公用)所能到达的最大范围ireturn;}// 取出前缀和的二进制表示从右往左数第k位v// 需要注意的是, 这个s[i]就是我们要存的东西!!!!!int v = s[i] >> k & 1;// 如果前一个节点存在当前节点没有的分支, 那就把当前节点的这个空的路径指过去, 这就相当于复制!if (p) tr[q][v ^ 1] = tr[p][v ^ 1];tr[q][v] = ++idx; // 现在才是正常trie树插入// 递归插入下一位二进制, tr[q][v]就是我们本轮插入的新节点// 而前面我们只复制了前一轮的不同v方向的路径, v方向的还没动过, 于是放到p里面等下一轮// 至于为什么可以放到下一轮, 因为当前q新插入的数字(二进制当前位)是v, 而p的这条路径也是v// 所以暂时不需要复制insert(i, k - 1, tr[p][v], tr[q][v]);// 下面是递归到所有点都插入完成才开始进行的, 所以能把最大max_id递归传递回去// 每个点的最大范围用子节点最大的值, 然后还能递归传递回去, 因为当前递归层// 的q, 就是上一层的tr[q][v], 观察易知每个节点都会有对应max_idmax_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);}int query(int r, int C, int l){// 选用合适的root, 就是第r-1个节点作为root(-1已在传参前完成)// 然后根据异或的前缀和性质才能保证在r左边int p = root[r];for (int i = 23; i >= 0; i--){// C是s[n] ^ x, 从高位到低位逐位检索二进制每一位上能跟C异或结果最大的数int v = C >> i & 1;// 自带判空功能如果没有点, max_id会为0, 那就肯定不能满足>=l// 而max_id又同时可以限制当前的点是在l r区间内// 另外, 如果tr[p][v^1]为空, 那么tr[p][v]就肯定不为空,并在l r区间, 因为根据// 插入的代码, 每个节点至少有一条当前s[i]的完整路径// 而如果tr[p][v^1]不为空但maxid小于l, 同理也能选取到tr[p][v]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(){scanf("%d%d", &n, &m);// 前缀和, 初始化root[0]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);s[i] = s[i - 1] ^ x; // 前缀和序列root[i] = ++idx;insert(i, 23, root[i - 1], root[i]);}char op[2];int l, r, x;while (m--){scanf("%s", op);if (op[0] == 'A'){scanf("%d", &x);n++;s[n] = s[n - 1] ^ x;root[n] = ++idx;insert(n, 23, root[n - 1], root[n]);}else{scanf("%d%d%d", &l, &r, &x);// 至少要包住第r个点, 所以用r-1, 否则会因为异或把root[r]抵消掉// l也同理printf("%d\n", query(r - 1, s[n] ^ x, l - 1));}}return 0;}
#include <iostream>#include <cstring>#include <algorithm>using namespace std;const int N = 6e5+10,M = N*24;int tr[M][2],idx,maxid[M],root[N];int s[N];int n,m;/*下面insert函数中注意if(lp){tr[p][u^1] = tr[lp][u^1];lp = tr[lp][u];}这段是非常巧妙的,试问自己一个问题,这棵trie树是如何保存到历史版本的关键就在于 if函数里面那两句理解一下: 这里的tr其实是一个指针,直接将现在的节点指向了,上一个版本的trie树中的节点而且还能保证不重不漏*/void insert(int k,int lp,int p,int x){maxid[p] = k;for (int i = 23; i >=0; i -- ){int u = x>>i&1;if(lp){tr[p][u^1] = tr[lp][u^1];lp = tr[lp][u];}tr[p][u] = ++idx;p = tr[p][u];maxid[p]=k;}}int query(int p,int x,int l){for (int i = 23; i >=0; i -- ){int u = x>>i&1;if(maxid[tr[p][u^1]]>=l){p = tr[p][u^1];}else{p = tr[p][u];}}return x^s[maxid[p]];}int main(){cin >> n >> m;maxid[0]=-1;root[0]=++idx;insert(0,root[0],root[0],0);for (int i = 1; i <= n; i ++ ){int x ;cin >> x ;s[i] = s[i-1]^x;root[i] = ++idx;insert(i,root[i-1],root[i],s[i]);}char op[2];int l,r,x;while (m -- ){scanf("%s", &op);if(op[0]=='A'){cin >> x;n++;root[n] = ++idx;s[n] = s[n-1]^x;insert(n,root[n-1],root[n],s[n]);}else{cin >> l >> r >> x;cout << query(root[r-1],s[n]^x,l-1) <<endl;}}}
255. 第K小数
给定长度为 N 的整数序列 A,下标为 1∼N。
现在要执行 M 次操作,其中第 i 次操作为给出三个整数 li,ri,ki,求 A[li],A[li+1],…,A[ri] (即 A 的下标区间 [li,ri])中第 ki 小的数是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含 N 个整数,表示整数序列 A。
接下来 M 行,每行包含三个整数 li,ri,ki,用以描述第 i 次操作。
输出格式
对于每次操作输出一个结果,表示在该次操作中,第 k 小的数的数值。
每个结果占一行。
输入样例:
7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3
输出样例:
5
6
3
#include <iostream>#include <cstring>#include <algorithm>#include <vector>using namespace std;const int N = 1e5 + 10;int n, m;int a[N];vector<int> nums; // 用于离散化struct Node {int l, r, cnt; // l和r分别是左儿子和右儿子的编号,cnt是在某个区间内元素个数// 而这个区间是默认的} tr[N * 4 + 17 * N];int root[N], idx;int find(int x) {return lower_bound(nums.begin(), nums.end(), x) - nums.begin();}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 q, int p, 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 (k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k);else return query(tr[q].r, tr[p].r, mid + 1, r, k - cnt);}int main() {cin >> n >> m;for (int i = 1; i <= n; i++) {cin >> a[i];nums.push_back(a[i]);}// 排序,去重sort(nums.begin(), nums.end());nums.erase(unique(nums.begin(), nums.end()), nums.end());// 插入各个元素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);cout << nums[query(root[r], root[l - 1], 0, nums.size() - 1, k)];}}
AC自动机
1282. 搜索关键词
给定 n 个长度不超过 50 的由小写英文字母组成的单词,以及一篇长为 m 的文章。
请问,其中有多少个单词在文章中出现了。
注意:每个单词不论在文章中出现多少次,仅累计 1 次。
输入格式
第一行包含整数 T,表示共有 T 组测试数据。
对于每组数据,第一行一个整数 n,接下去 n 行表示 n 个单词,最后一行输入一个字符串,表示文章。
出格式
对于每组数据,输出一个占一行的整数,表示有多少个单词在文章中出现。
数据范围
1≤n≤10^4,
1≤m≤10^6
输入样例:
1
5
she
he
say
shr
her
yasherhs
输出样例:
3
这道题跟平常的ac自动机有点不一样之处 就是 cnt[p]统计以p为后缀的所有单词的时候,在build else { ne[p] = tr[ne[t]][i]; q[++tt] = p; } 这块时不能直接写上 cnt[p]+=cnt[ne[p]],可能你会想到一种办法,那就是在主函数遍历那块写成 int j = str[i]-‘a’; p = tr[p][j]; // 走到了ac自动机的这个节点上 res += cnt[p]; int u = p; while(u){ cnt[u]=0; u = ne[u]; } 这样就因为cnt[p]包含了cnt[ne[p]], 所以要把它所有的ne[p]的cnt清零
但是,但是,但是,这也是不对的!!!
- 因为有可能有一个更长的字符串也包含了ne[p],那么这个字符串也应该减去ne[p],但是很显然减不掉
所以只能用下面这种解法
using namespace std;
const int N = 1e4 + 10, M = N * 55; int tr[M][26], idx; int cnt[M], ne[M]; int q[M];
void insert(string str) // 将str插入Trie中 { int p = 0; for (int i = 0; i < str.size(); i++) { int u = str[i] - ‘a’; if (!tr[p][u]) tr[p][u] = ++idx; p = tr[p][u]; } cnt[p]++; // 记录单词出现次数 }
void build() // 创建AC自动机 { int hh = 0, tt = -1; for (int i = 0; i < 26; i++) if (tr[0][i]) q[++tt] = tr[0][i]; while (hh <= tt) { int t = q[hh++]; for (int i = 0; i < 26; i++) { int p = tr[t][i]; if (!p) tr[t][i] = tr[ne[t]][i]; else { ne[p] = tr[ne[t]][i]; q[++tt] = p; } } } }
int main() { int T; cin >> T; while (T—) { memset(cnt, 0, sizeof cnt); memset(ne, 0, sizeof ne); memset(tr, 0, sizeof tr); idx = 0; int n; cin >> n; for (int i = 0; i < n; i++) { string s; cin >> s; insert(s); } build(); string str; cin >> str; // 看看文章中出现多少单词 int p = 0, res = 0; for (int i = 0; i < str.size(); i++) { // 枚举每个字母 int j = str[i] - ‘a’; p = tr[p][j]; // 走到了ac自动机的这个节点上 int q = p; while (q) { res += cnt[q]; cnt[q] = 0; q = ne[q]; } } cout << res << endl; }
}
<a name="RgTog"></a>### 1285. 单词某人读论文,一篇论文是由许多单词组成的。<br />但他发现一个单词会在论文中出现很多次,现在他想知道每个单词分别在论文中出现多少次。<br />**输入格式**<br />第一行一个整数 N,表示有多少个单词。<br />接下来 N 行每行一个单词,单词中只包含小写字母。<br />**输出格式**<br />输出 N 个整数,每个整数占一行,第 i 行的数字表示第 i 个单词在文章中出现了多少次。<br />**数据范围**<br />1≤N≤200,<br />所有单词长度的总和不超过 106。<br />**输入样例:**<br />3<br />a<br />aa<br />aaa<br />**输出样例:**<br />6<br />3<br />1<br />```cpp#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>using namespace std;const int N = 1000010;int n;int tr[N][26], f[N], idx;int q[N], ne[N];char str[N];int id[210];void insert(int x){int p = 0;for (int i = 0; str[i]; i ++ ){int t = str[i] - 'a';if (!tr[p][t]) tr[p][t] = ++ idx;p = tr[p][t];f[p] ++ ;}id[x] = p;}void build(){int hh = 0, tt = -1;for (int i = 0; i < 26; i ++ )if (tr[0][i])q[ ++ tt] = tr[0][i];while (hh <= tt){int t = q[hh ++ ];for (int i = 0; i < 26; i ++ ){int &p = tr[t][i];if (!p) p = tr[ne[t]][i];else{ne[p] = tr[ne[t]][i];q[ ++ tt] = p;}}}}int main(){scanf("%d", &n);for (int i = 0; i < n; i ++ ){scanf("%s", str);insert(i);}build();for (int i = idx - 1; i >= 0; i -- ) f[ne[q[i]]] += f[q[i]];for (int i = 0; i < n; i ++ ) printf("%d\n", f[id[i]]);return 0;}

