线段树入门
-
了解线段树
线段树是一种二叉搜索树,它将一个区间划分成一些较小的区间,最终划分成单元区间,每个单元区间对应线段树中的一个叶结点,表示线段上一个点。如下图。
-
线段树的优势
在线段树上进行的操作复杂度是 O(log2n)。
现在,用最朴朴素的方式,在一个一维数组 a[n] 中,我们要修改某一个点的值时,比如将原数组中 a[3]=3 改成 a[3]=6 ,只需要 a[3]=6; 即可,复杂度为O(1);若需要求 a[k] (0≤i≤k<j<n) 的和时,需就需要遍历数组,复杂度为O(n)。
虽然从单点修改操作看来线段树没有明显的优势,但是,当我们需要进行多次区间修改和区间查询操作时,再使用朴素方法时就是 O(n²) 的复杂度。 而使用线段树时,就是 O(nlog2n) 的复杂度。所以,再多次查询和多次修改的操作中,线段树是一个很有优势的数据结构。
通过线段树,就可以在 O(log2n) 的时间复杂度下完成区间查询、区间修改、单点查询、单点修改、区间最值等操作。
接下来以区间求和介绍线段树的思路和写法。 -
建树
#include<bits/stdc++.h> #define lson l, mid, rt<<1 //左子树 #define rson mid+1, r, rt<<1|1 //右子树 using namespace std; const int MAX = 100; //数据量 int sum[MAX<<2]; //采用顺序存储结构建树,注意建树所用空间是4*MAX void push_up(int rt){ //向上更新 sum[rt] = sum[rt<<1] + sum[rt<<1|1]; //根结点的值等于左右子树区间的值的和 } void build(int l, int r, int rt){ //l,r是区间范围,rt是根结点下标 if(l==r){ //当l==r时,就是叶节点了,相当于线段上的某一点 sum[rt] = 1; //初始化,根据需要决定 return ; } int mid = (l+r)>>1; build(lson); //递归创建左子树 build(rson); //递归创建右子树 push_up(rt); }
-
单点查询
//单点查询 int query(int x, int l, int r, int rt){ //x是要查询的点,l,r是线段区间,rt是树的根节点 if(l==r && l==x){ //达到叶结点,并且与要查询的点吻合 return sum[rt]; } int mid = (l+r)>>1; if(x<=mid) query(x, lson); //查询的点在左区间 else query(x, rson); //查询的点在右区间 }
-
区间查询
//区间查询 int query(int b, int e, int l, int r, int rt){ //b,e是求和区间,l,r是线段区间,rt是树的根节点 if(b<=l && e>=r){ //当前区间完全包含在所求区间内 return sum[rt]; //返回该结点的值,就是该结点覆盖线段上区间[l,r]的点的值的和 } int mid = (l+r)>>1, ans = 0; /* 这里理解一下,求区间和的时候分成三种情况,所求区间: 1.完全包含在左子树包含的区间[l, mid]内; 2.完全包含在右子树包含的区间[mid+1, r]内; 3.在左子树区间有一部分,右子树的区间有一部分,就要将所求区间分成两部分求和 */ if(l<=mid) ans += query(b, e, lson); if(e>=mid+1) ans += query(b, e, rson); return ans; }
//区间查询第二种容易理解的方式 int query(int b, int e, int l, int r, int rt){ //b,e是求和区间,l,r是线段区间,rt是树的根节点 if(b==l && e==r){ //当前区间与所求区间吻合 return sum[rt]; //返回该结点的值,就是该结点覆盖线段上区间[l,r]的点的值的和 } int mid = (l+r)>>1, ans = 0; //这里依旧分成三种情况 if(e<=mid) ans += query(b, e, lson); //完全包含在左子树包含的区间[l, mid]内 else if(l>=mid+1) ans += query(b, e, rson); //完全包含在右子树包含的区间[mid+1, r]内 else ans = query(b, mid, lson) + query(mid+1, e, rson); //在左子树区间有一部分,右子树的区间有一部分 return ans; }
-
单点更新
//单点更新 void update(int x, int l, int r, int rt){ //x需要更新的点的位置 if(l==r && l==x){ sum[rt] = newdata; //更新值 return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); //需要更新的点在左子树 else update(x, rson); //需要更新的点在右子树 }
-
区间更新
对于更新区间[l, r]内的值(区间内每一个点同时加上某一个数),如果使用普通的数组,只需要遍历一边即可,时间复杂度是 O(n) ,同样,如果使用线段树的结构,对区间内每个点的值进行更新时,需要对每个数(O(n))先进行查找(O(log2n))再更改,时间复杂度为O(nlog2n),反而高于普通方法,所以在线段树上进行区间更新时,并不是采用直接更新某些点的方式,而是采用更新区间并标记,在需要时进行标记下放的方式完成的。称为lazy-tag方法。
lazy-tag方法当修改的是一整个区间时,只对这个线段区间进行整体上的修改,其内部每个元素的内容先不做修改,只有当这部分线段的一致性被破坏时,才把变化的值传递给子区间。那么每次区间修改的时间复杂度就是O(log2n),N次区间修改操作的时间复杂度就是O(nlog2n)。
下面用图解的形式来说明。与前面保持一致,使用 sum[MAX] 存放树,新增一个 add[MAX] 用来保存相应节点的 lazy 值。
初始状态:
首先将区间 [1, 3] 加上 1
a.[1,3] 分成 [1, 2] 和 [3,3]两个区间
b.更新两个区间的和以及对应的 lazy 值 再将区间 [2,4] 加 1
a.区间 [2,4] 分为 [2,2] 和 [3,4] 两个区间。
b.更新 [2, 2] 时,经过 [1,2] ,[1,2] 区间的 lazy 值不为零,说明该区间的点更新过,需要先将 lazy 标记下放到两个子区间,然后标记清零。再继续深入。
c.[3,4] 恰好为需要修改的区间,直接更新区间和,并将 lazy 标记。
//标记下放 void push_down(int rt, int n){ //n是区间长度 add[rt<<1] += add[rt]; //更新子区间lazy add[rt<<1|1] += add[rt]; sum[rt<<1] += (n-(n>>1))*add[rt]; //更新子区间 sum[rt<<1|1] += (n>>1)*add[rt]; add[rt] = 0; //取消本层lazy } //区间更新 void update(int b, int e, int x, int l, int r, int rt){ if(b<=l && e>=r){ add[rt] += x; //lazy增加 sum[rt] += (r-l+1)*x; //区间和增加 return ; } push_down(rt, (r-l+1)); //lazy下放 int mid = (l+r)>>1; if(b<=mid) update(b, e, x, lson); //更新左区间 if(e>=mid+1) update(b, e, x, rson); //更新右区间 push_up(rt); }
-
离散化
当节点规模很大时,由于空间限制,很难在程序中建立二叉树时,就需要用到离散化,离散化实际上就是将大的二叉树压缩成较小的二叉树,但是压缩前后子区间的关系不变。
例如一块宣传栏,横向长度刻度为1→10,贴四张不同颜色的海报,他们和宣传栏等宽,长度分别为[1,3] 、[2,5]、[3,8]、[3,10] ,并且用后者覆盖前者,问最后能见几种颜色的海报。
离散化步骤如下:
(1)提取四张海报的八个顶点 1, 3, 2, 5, 3, 8, 3, 10
(2)排序并删除相同的端点 1, 2, 3, 5, 8, 10
(3)把原线段上的端点映射到新线段上:
新的 4 张海报为[1,3]、 [2,4]、 [3,5]、 [3,6] ,覆盖关系不变,新的宣传栏长度压缩到 6 。
练习
Lost Cows
A Simple Problem with Integers
敌兵布阵
Minimum Inversion Number
Just a Hook
I Hate It
Billboard
Ultra-QuickSort
Buy Tickets
Stars
Who Gets the Most Candies?
-
题意:给出2->n头牛前面有多少头比他编号少的数目,求出每头牛原来的编号,
-
思路:从后面向前遍历,对第m头牛前面有a头牛,表示此牛在剩余的牛中排名a+1.用线段树维护区间里面未知编号的牛的数量,找到满足条件的区间,区间右端点即为此牛的编号。找到后将此牛标记一下已知编号。
-
代码
#include<iostream> #include<stdio.h> using namespace std; int sum[17000]; int ans[8005], que[8005]; int q, ran; void build(int n, int l, int r){ if(l==r){ sum[n] = 1; return ; } int m = (l+r)>>1; build(2*n, l, m); build(2*n+1, m+1, r); sum[n] = sum[2*n] + sum[2*n+1]; } int query(int n, int l, int r){ if(l==r) return r; int m = (l+r)>>1; if(ran<=sum[2*n]){ return query(2*n, l, m); } else{ ran -= sum[2*n]; return query(2*n+1, m+1, r); } } void update(int n, int l, int r){ if(l==r){ sum[n]--; return ; } int m = (l+r)>>1; if(ran>m) update(2*n+1, m+1, r); else update(2*n, l, m); sum[n] = sum[2*n] + sum[2*n+1]; } int main(){ scanf("%d", &q); for(int i=1; i<q; i++) scanf("%d", &que[i]); que[0] = 0; build(1, 1, q); for(int i=q-1; i>=0; i--){ ran = que[i]+1; ans[i] = query(1, 1, q); ran = ans[i]; update(1, 1, q); } for(int i=0; i<q; i++) printf("%d\n", ans[i]); return 0; }
-
模板题,就是区间查询和区间修改。区间修改时,直接修改点必然会超时,这就用到了区间修改的 lazy 标记。还有就是注意一下数据范围,使用 long long 。
-
代码
#include<iostream> #include<stdio.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll long long using namespace std; const int MAXN = 1e5+10; ll sum[MAXN<<2]={0}, add[MAXN<<2]={0}; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void push_down(int rt, int m){ if(add[rt]){ add[rt<<1] += add[rt]; add[rt<<1|1] += add[rt]; sum[rt<<1] += (m-(m>>1))*add[rt]; sum[rt<<1|1] += (m>>1)*add[rt]; add[rt] = 0; } } void build(int l, int r, int rt){ add[rt] = 0; if(l == r){ scanf("%lld", &sum[rt]); return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int a, int b, ll c, int l, int r, int rt){ if(a<=l && b>=r){ sum[rt] += (r-l+1)*c; add[rt] += c; return ; } push_down(rt, r-l+1); int mid = (l+r)>>1; if(a<=mid) update(a, b, c, lson); if(b>mid) update(a, b, c, rson); push_up(rt); } ll query(int a, int b, int l, int r, int rt){ if(a<=l && b>=r) return sum[rt]; push_down(rt, r-l+1); int mid = (l+r)>>1; ll ans = 0; if(a<=mid) ans += query(a, b, lson); if(b>mid) ans += query(a, b, rson); return ans; } int main(){ int n, m; scanf("%d%d", &n, &m); build(1, n, 1); while(m--){ char str[2]; int a, b; ll c; scanf("%s", str); if(str[0] == 'C'){ scanf("%d%d%lld", &a, &b, &c); update(a, b, c, 1, n, 1); } else{ scanf("%d%d", &a, &b); printf("%lld\n", query(a, b, 1, n, 1)); } } return 0; }
-
思路:单点更新和区间求和。模板题
-
代码
#include<iostream> #include<stdio.h> using namespace std; #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 const int MAXN = 50005; int sum[MAXN<<2]={0}; //建树 void build(int l, int r, int rt){ if(l == r){ scanf("%d", &sum[rt]); return ; } int mid = (l+r)>>1; build(lson); build(rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //单点修改 void update(int i, int j, int l, int r, int rt){ if(l==r){ sum[rt] += j; return ; } int mid = (l+r)>>1; if(i<=mid) update(i, j, lson); else update(i, j, rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //区间查询 int query(int i, int j, int l, int r, int rt){ if(i==l && j==r) return sum[rt]; int mid = (l+r)>>1; if(j>=mid+1 && i<=mid) return query(i, mid, lson)+query(mid+1, j, rson); else if(j<=mid) return query(i, j, lson); else /*(i>=mid+1)*/ return query(i, j, rson); } int main(){ int m, n, i, j; string str; scanf("%d", &m); for(int k=1; k<=m; k++){ printf("Case %d:\n", k); scanf("%d", &n); build(1, n, 1); while(1){ cin>> str; if(str=="End") break; scanf("%d%d", &i, &j); if(str == "Query"){ printf("%d\n", query(i, j, 1, n, 1)); } else{ if(str=="Add") update(i, j, 1, n, 1); else update(i, 0-j, 1, n, 1); } } } return 0; }
-
给你一个数列,求这个数列的逆序数。这是一道求逆序数的模板题。
那么如何通过线段树求逆序数呢。首先我们知道一个数的逆序数的时候,就看在它的前面出现了几个比他大的数,那么这个数就是它的逆序数。所以,依照输入顺序,输入一个数后a后,就查找区间 [a+1, n] 中有几个数已经存在了,就是 a 的逆序数,然后标记 a 。找完所有数的逆序数再相加就可以了。 -
代码
#include<iostream> #include<stdio.h> #include<string.h> using namespace std; #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 const int MAXN = 5005; int sum[MAXN<<2]={0}, pre[MAXN], ma, ans; //建树 void build(int l, int r, int rt){ if(l == r) return ; int mid = (l+r)>>1; build(lson); build(rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //单点修改 void update(int x, int l, int r, int rt){ // cout<< x<< "--"<< l<< "--"<< r<< endl; if(l==r && l==x){ sum[rt] = 1; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //区间查询 int query(int i, int j, int l, int r, int rt){ if(i>j) return 0; if(i==l && j==r) return sum[rt]; int mid = (l+r)>>1; if(j>=mid+1 && i<=mid) return query(i, mid, lson)+query(mid+1, j, rson); else if(j<=mid) return query(i, j, lson); else return query(i, j, rson); } int main(){ int n; while(~scanf("%d", &n)){ memset(sum, 0, sizeof(sum)); ma = 0; build(1, n, 1); for(int i=0; i<n; i++){ scanf("%d", &pre[i]); update(pre[i]+1, 1, n, 1); ma += query(pre[i]+2, n, 1, n, 1); } ans = ma; for(int i=0; i<n; i++){ ma += (n-pre[i]-1)-pre[i]; if(ma<ans) ans = ma; } printf("%d\n", ans); } return 0; }
-
模板题区间修改。但这道题中的区间修改与上面讲到的区间修改不同,此题中的区间修改式改变区间中的值为给定值,不是累加关系。
-
代码
#include<iostream> #include<stdio.h> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll int using namespace std; const int MAXN = 1e5+10; ll sum[MAXN<<2]={0}, add[MAXN<<2]={0}; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void push_down(int rt, int m){ if(add[rt]){ add[rt<<1] = add[rt]; add[rt<<1|1] = add[rt]; sum[rt<<1] = (m-(m>>1))*add[rt]; sum[rt<<1|1] = (m>>1)*add[rt]; add[rt] = 0; } } void build(int l, int r, int rt){ add[rt] = 0; if(l == r){ sum[rt] = 1; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int a, int b, ll c, int l, int r, int rt){ if(a<=l && b>=r){ sum[rt] = (r-l+1)*c; add[rt] = c; return ; } push_down(rt, r-l+1); int mid = (l+r)>>1; if(a<=mid) update(a, b, c, lson); if(b>mid) update(a, b, c, rson); push_up(rt); } int main(){ int k; scanf("%d", &k); for(int i=1; i<=k; i++){ int N, Q, x, y, z; scanf("%d%d", &N, &Q); build(1, N, 1); while(Q--){ scanf("%d%d%d", &x, &y, &z); update(x, y, z, 1, N, 1); } printf("Case %d: The total value of the hook is %d.\n", i, sum[1]); memset(sum, 0, sizeof(sum)); memset(add, 0, sizeof(add)); } return 0; }
-
模板题,单点修改+区间最值。
-
代码
#include<iostream> #include<stdio.h> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll int using namespace std; const int MAXN = 2e5+10; ll sum[MAXN<<2]={0}; void push_up(int rt){ sum[rt] = sum[rt<<1]>sum[rt<<1|1]? sum[rt<<1]:sum[rt<<1|1]; } void build(int l, int r, int rt){ if(l == r){ scanf("%d", &sum[rt]); return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int a, int b, int l, int r, int rt){ if(l==r){ sum[rt] = b; return ; } int mid = (l+r)>>1; if(a<=mid) update(a, b, lson); else update(a, b, rson); push_up(rt); } int query(int a, int b, int l, int r, int rt){ if(a<=l && b>=r) return sum[rt]; int mid = (l+r)>>1; int x=0, y=0; if(a<=mid) x = query(a, b, lson); if(b>=mid+1) y = query(a, b, rson); return x>y? x:y; } int main(){ int n, m, a, b; char c; while(~scanf("%d%d", &n, &m)){ build(1, n, 1); while(m--){ scanf(" %c%d%d", &c, &a, &b); if(c=='Q') printf("%d\n", query(a, b, 1, n, 1)); else update(a, b, 1, n, 1); } } return 0; }
-
单点修改+区间查询(查找到某一点)的模板题。这道题难点在如何使用线段树,方法就是:由于海报的高都是一个单位,所以可以将海报板按单位长度分成一行一行,线段树的每个单元区间(就是线段树的叶节点,也是线段上的一个点)代表一行,单元区间的值表示这个行剩下的长度。因为贴海报的时候是依据上和左优先,其次是下和右的原则,所以只需要从线段上从走向有查找,就相当于从海报板上从上向下查找,对于一行,看是否可以贴(剩余长度是否够)就可以,实际上是不用考虑从左向右的。所以,想明白之后,就是单点修改+区间查询的模板题。
-
代码
#include<iostream> #include<stdio.h> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 #define ll int using namespace std; const int MAXN = 2e5+5; ll sum[MAXN<<2]; int h, w, n, pos, x; void push_up(int rt){ sum[rt] = sum[rt<<1]>sum[rt<<1|1]? sum[rt<<1]:sum[rt<<1|1]; //保存区间最大值,当该区间最大值比海报长度小时,就肯定不贴在该区间,也就不用搜索了。 } void build(int l, int r, int rt){ if(l == r){ sum[rt] = w; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void update(int l, int r, int rt){ if(l==r){ sum[rt] -= x; return ; } int mid = (l+r)>>1; if(pos<=mid) update(lson); else update(rson); push_up(rt); } bool query(int l, int r, int rt){ if(l==r){ if(x<=sum[rt]){ pos = l; return true; } else return false; } int mid = (l+r)>>1; if(x<=sum[rt<<1]) return query(lson); else return query(rson); } int main(){ while(~scanf("%d%d%d", &h, &w, &n)){ h = min(h, n); build(1, h, 1); while(n--){ scanf("%d", &x); if(query(1, h, 1)){ printf("%d\n", pos); update(1, h, 1); } else printf("-1\n"); } } return 0; }
-
逆序数+离散化。这道题的突破点在于求逆序数。因为排序时是通过相邻元素两两交换完成的,所以实际上排序完成后的交换次数就是给定初始序列的逆序数。实际上就是通过线段树求逆序数的问题了。另外就是离散化的问题。由于要排序的数据可能很大,如果只根据数据建立线段树是行不通的,可以尝试一下,这就还需要先进行离散化,再求逆序数。
-
代码
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 5e5+5; int sum[MAX<<2]; int n; __int64 num; typedef struct{ int key1, key2; int value; }node; node in[MAX]; bool cmp1(node n1, node n2){ return n1.value<n2.value; } bool cmp2(node n1, node n2){ return n1.key1<n2.key1; } void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void update(int x, int l, int r, int rt){ if(l==r){ sum[rt]=1; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); push_up(rt); } int query(int b, int e, int l, int r, int rt){ if(b>e) return 0; if(b<=l && e>=r) return sum[rt]; int mid = (l+r)>>1; int ans = 0; if(b<=mid) ans += query(b, e, lson); if(e>=mid+1) ans += query(b, e, rson); push_up(rt); return ans; } int main(){ while(1){ scanf("%d", &n); if(n==0) break; memset(sum, 0, sizeof(sum)); num = 0; for(int i=1; i<=n; i++){ in[i].key1 = i; scanf("%d", &in[i].value); } stable_sort(in+1, in+n+1, cmp1); for(int i=1; i<=n; i++) in[i].key2 = i; stable_sort(in+1, in+n+1, cmp2); for(int i=1; i<=n; i++){ num += query(in[i].key2+1, n, 1, n, 1); update(in[i].key2, 1, n, 1); } printf("%I64d\n", num); } return 0; }
-
买票插队,每行两个数 a 和 b ,表示编号为 b 的人插在了队伍里第 a 个人后面,问最后的队列顺序(按顺序输出每个人的编号)。这道题和奶牛排序的问题类似,由于最后一个人插进队伍之后,这个人在队伍里的位置就确定了,就可以不考虑这个人对其他人的位置的影响了,所以,采用从后向前遍历的方法来确定某一个人的位置,使用线段树维护区间和表示该区间有多少个人没确定位置就可以了。
-
代码
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 2e5+5; int sum[MAX<<2]; int n, p[MAX], v[MAX], ans[MAX], i; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void build(int l, int r, int rt){ if(l==r){ sum[rt] = 1; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } void query(int x, int l, int r, int rt){ if(l==r){ sum[rt] = 0; ans[l] = v[i]; return ; } int mid = (l+r)>>1; if(x<=sum[rt<<1]) query(x, lson); else query(x-sum[rt<<1], rson); push_up(rt); } int main(){ while(~scanf("%d", &n)){ build(1, n, 1); for(int i=0; i<n; i++) scanf("%d%d", &p[i], &v[i]); for(i=n-1; i>=0; i--) query(p[i]+1, 1, n, 1); for(int i=1; i<=n; i++) printf("%d ", ans[i]); cout<< endl; } return 0; }
-
在直角坐标系中,每一个星星占据坐标系上一个点,对于一个星星,它的 level 定义为标系上在它正下方、正左方以及左下方范围内的星星的个数。由于输入采取的原则是按照坐标从左向右、从上到下的原则输入的,忽略纵坐标,只考虑横坐标,相当于在线段上的点上重复放置星星,对于星星 (x, y) 只需要查询区间 [1, x] 内在它之前一共放置了多少个星星就可以了。
-
代码
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 32e3+5; int sum[MAX<<2], ans[MAX]; int n, x, y; void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } //void build(int l, int r, int rt){ // if(l==r){ // sum[rt] = 0; // return ; // } // int mid = (l+r)>>1; // build(lson); // build(rson); // push_up(rt); //} int query(int b, int e, int l, int r, int rt){ if(b<=l && e>=r){ return sum[rt]; } int mid = (l+r)>>1; int ans = 0; if(b<=mid) ans += query(b, e, lson); if(e>=mid+1) ans += query(b, e, rson); return ans; } void update(int x, int l, int r, int rt){ if(l==r){ sum[rt]++; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); push_up(rt); } int main(){ scanf("%d", &n); for(int i=0; i<n; i++){ scanf("%d%d", &x, &y); ans[query(1, x+1, 1, 32e3+1, 1)]++; update(x+1, 1, 32e3+1, 1); } for(int i=0; i<n; i++) printf("%d\n", ans[i]); return 0; }
-
约瑟夫环 + 打表 + 线段树。
如果一个小孩儿第 n 个出队,那么他得到的糖果就是 n 的因子的个数。然后出队的这个小孩儿手中有一张卡片,写了下一个倒霉蛋的位置 a ,规则是:a > 0,从他开始顺时针第 a 个人出队; a < 0,从他开始逆时针第 a 个人出队。
要找到糖果数最多的小孩儿,就要找n个小孩中第几个出队的因子数最多,也就是 1 → n 中哪个数的因子最多。
为了避免超时,就要提前打表,找出每个数的因子个数。打表的时候也要考虑打表的方法,我采用的方法是,对于一个数,它的所有倍数因子数加一。
然后就是用线段树模拟约瑟夫环。我采用的方法是看要出队的小孩儿前面(包含这个小孩儿)有几个小孩儿,从而确定要出队的小孩儿的位置。至于有几个小孩儿,拿出笔画画很容易发现规律,这里我就直接写出来了:
卡片上的数是正数:num = ((num-1+children[id].k-1)%sum[1] + sum[1])%sum[1]+1;
负数:num = ((num-1+children[id].k)%sum[1]+sum[1])%sum[1]+1;
这里注意取模 +1 就好,取模是为了模拟循环,+1 是线段树的点是从 1 开始的。 -
代码
#include<iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define lson l, mid, rt<<1 #define rson mid+1, r, rt<<1|1 using namespace std; const int MAX = 5e5+5; int sum[MAX<<2], ans[MAX]; int n, p, mod; struct{ int k; char name[11]; }children[MAX]; void init(){ for(int i=1; i<=MAX; i++){ ans[i]++; for(int j=i+i; j<=MAX; j+=i){ ans[j]++; } } } int the_max(int n){ int m = ans[1], j = 1; for(int i=2; i<=n; i++){ if(m<ans[i]){ m = ans[i]; j = i; } } return j; } void push_up(int rt){ sum[rt] = sum[rt<<1] + sum[rt<<1|1]; } void build(int l, int r, int rt){ if(l==r){ sum[rt] = 1; return ; } int mid = (l+r)>>1; build(lson); build(rson); push_up(rt); } int query(int x, int l, int r, int rt){ if(l==r) return r; int mid = (l+r)>>1; if(x<=sum[rt<<1]) query(x, lson); else query(x-sum[rt<<1], rson); } void update(int x, int l, int r, int rt){ if(l==r && l==x){ sum[rt] = 0; return ; } int mid = (l+r)>>1; if(x<=mid) update(x, lson); else update(x, rson); push_up(rt); } int main(){ init(); while(~scanf("%d%d", &n, &p)){ for(int i=1; i<=n; i++) scanf("%s%d", children[i].name, &children[i].k); build(1, n, 1); int k = the_max(n), id = p, num = p; for(int i=1; i<k; i++){ update(id, 1, n, 1); if(sum[1]==0) break; if(children[id].k<0) num = ((num-1+children[id].k)%sum[1]+sum[1])%sum[1]+1; else num = ((num-1+children[id].k-1)%sum[1] + sum[1])%sum[1]+1; id = query(num, 1, n, 1); } printf("%s %d\n", children[id].name, ans[k]); } return 0; }