样例跑下,对于数列
1 3 4 5 6 7 10 12
求第3小的
对于整个数列,最小值为1,最大值为12,中间值为6
我们将小于等于6的数字找出来,发现有5个
说明我们二分枚举的答案太大了,所以小于它的数字过多。
于是将小于6的数字变成一个数列
1 3 4 5 6
再取1与6中间值为3
发现小于等于3的数字只有2个,达不到我们的要求
说明答案大于3.于是应该在数列
4 5 6
中找第1小的
此时再取4与6的中间值为5
发现小于等于5的有2个,大于1,说明值又取大了
于是将小于等于5的数字找出来
4 5
再取4和5的中间值为4
小于等于4的只有1个。于是数字序列只有4.答案区间也只有[4,4]
于是最后结果为4
如果是有多个询问,注意加入值的操作在前面,询问在后面。
经过二分后,在每个区间内部,仍然是加值操作在前,询问操作在后面
一般来讲我们会把每个有权值的位置放到一个队列里,然后所有操作也放在这个队列里,但是放在权值之后。
然后我们二分一个值,将所有这个值下仍能合法的询问丢到右区间处理 (也就是它们的二分区间肯定都变成了 [mid+1,r] ) ,同时将大于等于 mid 的权值和修改权值丢到右边去,因为它们会对且仅对右区间的询问产生贡献。
左区间同理。
当我们的二分区间 l==r 时,更新所有二分区间为这个的询问的答案。
大致就是一个函数 Solve(L,R,l,r) ,代表我们现在处理的是 [L,R] 的操作或权值序列,它们对应的二分区间都是 [l,r] 。每次求出有 len1 个操作和权值要丢到左区间,len2 个丢到右区间,那我们就拿出这些操作并重新摆放位置,确定那 len1 个操作都在 [L,L+len1−1] 这段区间,且按照操作的时间顺序摆放,另外 len2 个操作同理。
那么我们的递归处理就是 Solve(L,L+len1−1,l,mid) 与 Solve(L+len,R,mid+1,r) 了。
POJ2104 第K小数
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 100010, INF = 1e9; struct rec {int op, x, y, z;} q[2 * N], lq[2 * N], rq[2 * N]; int n, m, t, c[N], ans[N]; int ask(int x) { int y = 0; for (; x; x -= x & -x) y += c[x]; return y; } void change(int x, int y) { for (; x <= n; x += x & -x) c[x] += y; } void solve(int lval, int rval, int st, int ed) { if (st > ed) return; if (lval == rval) //值域达到边界 { for (int i = st; i <= ed; i++) if (q[i].op > 0) //如果是一次询问操作 ans[q[i].op] = lval; return; } int mid = (lval + rval) >> 1; //!!!二分枚举一个值出来 //将小于等于mid的数字放在lq,大于的放在rq int lt = 0, rt = 0; for (int i = st; i <= ed; i++) { if (q[i].op == 0) { //如果是一次赋值操作 if (q[i].y <= mid) //如果要所赋的值小于mid change(q[i].x, 1), lq[++lt] = q[i]; //在第x个位置上加上1 else rq[++rt] = q[i]; } else //如果是一次询问操作 { int cnt = ask(q[i].y) - ask(q[i].x - 1); //cnt统计在第i个询问中,小于mid的数字有多少个 if (cnt >= q[i].z) lq[++lt] = q[i]; //答案落在左边 else q[i].z -= cnt, rq[++rt] = q[i]; //答案落在右边 } } for (int i = ed; i >= st; i--) { // 还原树状数组 //可以设想下mid的值较大,此时就会在bit中加入一些数字 //但事实上查找范围落在左边,于是要清空当前的操作,后面再来加入 if (q[i].op == 0 && q[i].y <= mid) change(q[i].x, -1); } for (int i = 1; i <= lt; i++) q[st + i - 1] = lq[i]; for (int i = 1; i <= rt; i++) q[st + lt + i - 1] = rq[i]; solve(lval, mid, st, st + lt - 1); solve(mid + 1, rval, st + lt, ed); } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { int val; scanf("%d", &val); // a[i]=val,将其看成一个赋值操作 q[++t].op = 0, q[t].x = i, q[t].y = val; } for (int i = 1; i <= m; i++) { int l, r, k; scanf("%d%d%d", &l, &r, &k); // 询问[l,r]之间第k小的数字 q[++t].op = i, q[t].x = l, q[t].y = r, q[t].z = k; } solve(-INF, INF, 1, t); for (int i = 1; i <= m; i++) printf("%d ", ans[i]); }
Sol2:可持久化线段树的做法
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 100010, INF = 1e9; struct SegmentTree { int lc, rc; // 左右子节点编号 int sum; } tree[N * 20]; int n, m, t, tot, a[N], b[N], root[N]; int build(int l, int r) { int p = ++tot; // 新建一个节点,编号为p,代表当前区间[l,r] tree[p].sum = 0; if (l == r) return p; int mid = (l + r) >> 1; tree[p].lc = build(l, mid); tree[p].rc = build(mid + 1, r); return p; } int insert(int now, int l, int r, int x, int delta) { int p = ++tot; tree[p] = tree[now]; // 新建一个副本 if (l == r) { tree[p].sum += delta; // 在副本上修改 return p; } int mid = (l + r) >> 1; if (x <= mid) tree[p].lc = insert(tree[now].lc, l, mid, x, delta); else tree[p].rc = insert(tree[now].rc, mid + 1, r, x, delta); tree[p].sum = tree[tree[p].lc].sum + tree[tree[p].rc].sum; return p; } //在p,q两个节点上,值域为[l,r],求第k小数 int ask(int p, int q, int l, int r, int k) { if (l == r) return l; // 找到答案 int mid = (l + r) >> 1; int lcnt = tree[tree[p].lc].sum - tree[tree[q].lc].sum; // 值在[l,mid]中的数有多少个 if (k <= lcnt) return ask(tree[p].lc, tree[q].lc, l, mid, k); else return ask(tree[p].rc, tree[q].rc, mid + 1, r, k - lcnt); } int main() { cin >> n >> m; for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); b[++t] = a[i]; } sort(b + 1, b + t + 1); // 离散化 t = unique(b + 1, b + t + 1) - (b + 1); root[0] = build(1, t); // 关于离散化后的值域建树 for (int i = 1; i <= n; i++) { int x = lower_bound(b + 1, b + t + 1, a[i]) - b; // 离散化后的值 root[i] = insert(root[i - 1], 1, t, x, 1); // 值为x的数增加1个 } for (int i = 1; i <= m; i++) { int l, r, k; scanf("%d%d%d", &l, &r, &k); int ans = ask(root[r], root[l - 1], 1, t, k); printf("%d ", b[ans]); // 从离散化后的值变回原值 } }
zju2112
给定一个含有n个数的序列a[1],a[2],a[3]……a[n],程序必须回答这样的询问:对于给定的i,j,k,在a[i],a[i+1],a[i+2]……a[j]中第k小的数是多少(1≤k≤j-i+1),并且,你可以改变一些a[i]的值,改变后,程序还能针对改变后的a继续回答上面的问题。你需要编一个这样的程序,从输入文件中读入序列a,然后读入一系列的指令,包括询问指令和修改指令。对于每一个询问指令,你必须输出正确的回答。
输入
第一行有两个正整数n(1≤n≤10000),m(1≤m≤10000)。分别表示序列的长度和指令的个数。第二行有n个数,表示a[1],a[2]……a[n],这些数都小于10^9。接下来的m行描述每条指令,每行的格式是下面两种格式中的一种。 Q i j k 或者 C i t Q i j k (i,j,k是数字,1≤i≤j≤n, 1≤k≤j-i+1)表示询问指令,询问a[i],a[i+1]……a[j]中第k小的数。C i t (1≤i≤n,0≤t≤10^9)表示把a[i]改变成为t。
输出
对于每一次询问,你都需要输出他的答案,每一个输出占单独的一行。
样例
输入复制
5 3
3 2 1 4 7
Q 1 4 3
C 2 6
Q 2 5 3
输出复制
3
6
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 100010, INF = 1e9; struct rec {int op, x, y, z;} q[3 * N], lq[3 * N], rq[3 * N]; int T, n, m, t, p, a[N], c[N], ans[N]; //树状数组 int ask(int x) { int y = 0; for (; x; x -= x & -x) y += c[x]; return y; } void change(int x, int y) { for (; x <= n; x += x & -x) c[x] += y; } //lval和rval代表值域,st和ed代表操作序列区间 void solve(int lval, int rval, int st, int ed) { if (st > ed) return; if (lval == rval) { for (int i = st; i <= ed; i++) if (q[i].op > 0) ans[q[i].op] = lval; //询问 return; } int mid = (lval + rval) >> 1; //对值域二分 int lt = 0, rt = 0; //左右操作个数 for (int i = st; i <= ed; i++) { //序列区间 if (q[i].op <= 0) { // 代表修改,-1为去掉一个值,0为增加一个值 if (q[i].y <= mid) change(q[i].x, q[i].z), lq[++lt] = q[i]; //分治操作序列 else rq[++rt] = q[i]; } else { // 是一次询问 int cnt = ask(q[i].y) - ask(q[i].x - 1); //下标区间 [li,ri] 中不大于mid的数有多少个,记为 cnt。 if (cnt >= q[i].z) lq[++lt] = q[i]; //若 ki<=ci,则把该询问加入到序列 lq 中 else q[i].z -= cnt, rq[++rt] = q[i]; //否则,令 ki-=ci,将其加入到序列 rq 中 } } for (int i = ed; i >= st; i--) { // 还原树状数组 if (q[i].op <= 0 && q[i].y <= mid) change(q[i].x, -q[i].z); } for (int i = 1; i <= lt; i++) q[st + i - 1] = lq[i]; //把lq和rq拷贝回原操作序列的st--ed位置 for (int i = 1; i <= rt; i++) q[st + lt + i - 1] = rq[i]; solve(lval, mid, st, st + lt - 1); //递归求解 solve(mid + 1, rval, st + lt, ed); } int main() { cin >> n >> m; t = p = 0; for (int i = 1; i <= n; i++) { int val; scanf("%d", &val); // 等价于在第i个位置上加入一个数val q[++t].op = 0, q[t].x = i, q[t].y = val, q[i].z = 1; a[i] = val; } for (int i = 1; i <= m; i++) { char op[2]; scanf("%s", op); if (op[0] == 'Q') { int l, r, k; scanf("%d%d%d", &l, &r, &k); // 记录一次询问 q[++t].op = ++p, q[t].x = l, q[t].y = r, q[t].z = k; } else { int x, y; scanf("%d%d", &x, &y); // 去掉原来的数a[x] q[++t].op = -1, q[t].x = x, q[t].y = a[x], q[t].z = -1; // 在第x个位置上加入一个新的数y q[++t].op = 0, q[t].x = x, q[t].y = y, q[t].z = 1; a[x] = y; } } // 基于值域对t=n+m个操作进行整体分治 solve(0, INF, 1, t); for (int i = 1; i <= p; i++) printf("%d ", ans[i]); }
#include<cstdio> #define maxn 30010 #define INF 1000000000 #define lowbit(i) (i&-i) using namespace std; int n,m,a[maxn],cnt,tmp[maxn]; int ans[maxn],tr[maxn]; char cmd[10]; bool flag[maxn]; struct query{ int x,y,k,c,o; }q[maxn],q1[maxn],q2[maxn]; void add(int pos,int val) { for(int i=pos;i<=n;i+=lowbit(i)){ tr[i]+=val; } } int ask(int pos) { int ret=0; for(int i=pos;i>0;i-=lowbit(i)){ ret+=tr[i]; } return ret; } void devide(int head,int tail,int l,int r) { if(head>tail) return; if(l==r) { for(int i=head;i<=tail;i++) { if(q[i].c==3) { ans[q[i].o]=l; } } return; } int mid=(l+r)>>1; for(int i=head;i<=tail;i++) { if(q[i].c==1&&q[i].y<=mid) add(q[i].x,1); if(q[i].c==2&&q[i].y<=mid) add(q[i].x,-1); if(q[i].c==3) tmp[i]=ask(q[i].y)-ask(q[i].x-1); } for(int i=head;i<=tail;i++) { if(q[i].c==1&&q[i].y<=mid) add(q[i].x,-1); if(q[i].c==2&&q[i].y<=mid) add(q[i].x,1); } int l1=0,l2=0; for(int i=head;i<=tail;i++) { if(q[i].c==3) //如果是询问操作的话 { if(tmp[i]>=q[i].k) //答案落在左区间 { q1[++l1]=q[i]; } else { q[i].k-=tmp[i]; q2[++l2]=q[i]; } } else //如果是赋值操作的话 { if(q[i].y<=mid) { q1[++l1]=q[i]; } else { q2[++l2]=q[i]; } } } for(int i=1;i<=l1;i++) q[head+i-1]=q1[i]; for(int i=1;i<=l2;i++) q[head+l1+i-1]=q2[i]; devide(head,head+l1-1,l,mid); devide(head+l1,tail,mid+1,r); return; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); q[++cnt]=(query){i,a[i],0,1,0}; } for(int i=1;i<=m;i++) { scanf("%s",cmd); if(cmd[0]=='Q') //询问操作 { int x,y,k; scanf("%d%d%d",&x,&y,&k); q[++cnt]=(query){x,y,k,3,i}; //区间[x,y]第k大,3代表询问操作,i代表第i个询问 flag[i]=1; }else{ int x,y; scanf("%d%d",&x,&y); q[++cnt]=(query){x,a[x],0,2,0}; q[++cnt]=(query){x,y,0,1,0}; a[x]=y; //! } } devide(1,cnt,0,INF); for(int i=1;i<=m;i++){ if(flag[i]) printf("%d ",ans[i]); } }
整体二分习 题表
PKU2104 K-th Number
BZOJ2738 矩阵乘法
BZOJ2527 [Poi2011]Meteors
BZOJ3110 [Zjoi2013]K大数查询
BZOJ4009 [HNOI2015]接水果
还要再研究的:
https://www.cnblogs.com/AKMer/category/1397613.html