线段树练习——单点更新(一)
hdu1166 http://acm.hdu.edu.cn/showproblem.php?pid=1166
题意:
中文不说了,大体意思是给定n个数;
接下来每行有一条命令,命令有4种形式:
(1) Add i j,i和j为正整数,表示第i个营地增加j个人(j不超过30)
(2)Sub i j ,i和j为正整数,表示第i个营地减少j个人(j不超过30);
(3)Query i j ,i和j为正整数,i<=j,表示询问第i到第j个营地的总人数;
(4)End 表示结束,这条命令在每组数据最后出现;
很纯的单点更新题目:
#include <cstdio> #include <cstring> #include <iostream> #define maxn 50002 using namespace std; int a[maxn*4]; void pushup(int rt) { a[rt] = a[rt<<1] + a[rt<<1|1]; } void build(int l,int r,int rt) { if (l == r) { scanf("%d",&a[rt]); return ; } int m = (l + r)>>1; build(l,m,rt<<1); build(m + 1,r,rt<<1|1); pushup(rt); } void update(int pos,int sc,int l,int r,int rt) { if (l == r) { a[rt] += sc; return ; } int m = (l + r)>>1; if (pos <= m) update(pos,sc,l,m,rt<<1); else update(pos,sc,m + 1,r,rt<<1|1); pushup(rt); } int query(int L,int R,int l,int r,int rt) { if (l >= L && r <= R) { return a[rt]; } int res = 0; int m = (l + r)>>1; if (L <= m) res += query(L,R,l,m,rt<<1); if (R > m) res += query(L,R,m + 1,r,rt<<1|1); return res; } int main() { int t,x,y,n; int cas = 1; char op[6]; scanf("%d",&t); while (t--) { printf("Case %d:\n",cas++); scanf("%d",&n); build(1,n,1); while (1) { scanf("%s",op); if (op[0] == 'E') break; scanf("%d%d",&x,&y); if (op[0] == 'A') update(x,y,1,n,1); else if (op[0] == 'S') update(x,-y,1,n,1); else printf("%d\n",query(x,y,1,n,1)); } } return 0; }
hdu 1754 http://acm.hdu.edu.cn/showproblem.php?pid=1754
中文题目,同样很纯的单点更新的题目,只不过不是求区间和罢了,而是求区间最值问题了。
注意:query时比较的是L,R。。
#include <iostream> #include <cstring> #include <cstdio> #define maxn 2000002 using namespace std; int val[maxn*4]; void pushup(int rt) { val[rt] = max(val[rt<<1],val[rt<<1|1]); } void build(int l,int r,int rt) { if (l == r) { scanf("%d",&val[rt]); return ; } int m = (l + r)>>1; build(l,m,rt<<1); build(m + 1,r,rt<<1|1); pushup(rt); } void update(int pos,int sc,int l,int r,int rt) { if (l == r) { val[rt] = sc; return ; } int m = (l + r)>>1; if (pos <= m) update(pos,sc,l,m,rt<<1); else update(pos,sc,m + 1,r,rt<<1|1); pushup(rt); } int query(int L,int R,int l,int r,int rt) { if (l >= L && r <= R) { return val[rt]; } int res = 0; int m = (l + r)>>1; if (L <= m) res = max(res,query(L,R,l,m,rt<<1)); if (R > m) res = max(res,query(L,R,m + 1,r,rt<<1|1)); return res; } int main() { int n,q; int x,y; char op[2]; while (~scanf("%d%d",&n,&q)) { build(1,n,1); while (q--) { scanf("%s%d%d",op,&x,&y); if (op[0] == 'U') update(x,y,1,n,1); else printf("%d\n",query(x,y,1,n,1)); } } return 0; }
hdu 1394 http://acm.hdu.edu.cn/showproblem.php?pid=1394
题意:给定一个序列a[n],求将每个数a[i] (i从1到n)移到最后形成的逆序数的最小值(注意这里也要包括不移动时逆序数);
逆序数定义就是 is the number of pairs (ai, aj) that satisfy i < j and ai > aj.
首先用线段树结构的叶子节点存储每个位置的值是否出现,初始化为0都未出现,若出现则update为1,然后单点向上更新存储这一区间相应位置出现的个数。每次出现一个新数首先query然后再更新树。当这些数输入完毕,即得到没有移动时的逆序数,这里公式的推导:若将v[i]移到最后,则总的逆序数sum要加上比v[i]大的数的个数(n - v[i])减去比v[i]小的数的个数(v[i] - 1)注意:我的处理时从1开始到n的。
#include <iostream> #include <cstring> #include <cstdio> #define maxn 5007 using namespace std; int val[maxn*4],v[maxn]; void pushup(int rt) { val[rt] = val[rt<<1] + val[rt<<1|1]; } void update(int pos,int l,int r,int rt) { if (l == r) { val[rt] = 1; return ; } int m = (l + r)>>1; if (pos <= m) update(pos,l,m,rt<<1); else update(pos,m + 1,r,rt<<1|1); pushup(rt); } int query(int L,int R,int l,int r,int rt) { if (l >= L && r <= R) return val[rt]; int res = 0; int m = (l + r)>>1; if (L <= m) res += query(L,R,l,m,rt<<1); if (R > m) res += query(L,R,m + 1,r,rt<<1|1); return res; } int main() { int n; while (~scanf("%d",&n)) { int sum = 0; for (int i = 0; i < 4*maxn; ++i) val[i] = 0; for (int i = 0; i < n; ++i) { scanf("%d",&v[i]); sum += query(v[i] + 1,n,1,n,1); update(v[i] + 1,1,n,1); } int tmp = sum; for (int i = 0; i < n; ++i) { tmp = tmp + (n - v[i] - 1) - (v[i]); sum = min(sum,tmp); } printf("%d\n",sum); } return 0; }
hdu 2795 http://acm.hdu.edu.cn/showproblem.php?pid=2795
题意:
校园的广告片是h*w的矩形,现在给你n个1*wi的广告,往上面贴,必须满足如果最顶部宽度满足的话,一定要贴在最顶部(贴的广告都是靠最左的而且不会覆盖)。问给定的n个广告序列,被贴在第几行;
思路:
给定的h w 到了10^9很大,但是给定的n相对来说是小的,思考可知我们要以h建立线段树结构,而当出现h>n时,无需建立h个只需建立n个即可(此时如果一行贴一个的话肯定能把n个广告全部贴满)。建立线段树结构h个叶子节点存储w宽度,单点向上更新存放最大宽度,每次插入时检查该区间的最大宽度是否可以放下,可放即往下寻找。
#include <iostream> #include <cstring> #include <cstdio> #define maxn 200002 using namespace std; int val[maxn*4],w; void pushup(int rt) { val[rt] = max(val[rt<<1],val[rt<<1|1]); } void build(int l,int r,int rt) { if (l == r) { val[rt] = w; return ; } int m = (l + r)>>1; build(l,m,rt<<1); build(m + 1,r,rt<<1|1); pushup(rt); } void query(int x,int l,int r,int rt) { if (l == r) { printf("%d\n",l); val[rt] -= x; return; } int m = (l + r)>>1; if (x <= val[rt<<1]) query(x,l,m,rt<<1); else query(x,m + 1,r,rt<<1|1); pushup(rt); } int main() { int h,n; int x; while (~scanf("%d%d%d",&h,&w,&n)) { if (h > n) h = n; build(1,h,1); while (n--) { scanf("%d",&x); if (val[1] < x) puts("-1"); else query(x,1,h,1); } } return 0; }
hdu 4217 http://acm.hdu.edu.cn/showproblem.php?pid=4217
CZ做的一道题目,我帮忙看了看。
题意:给定N个数(1---N),K个操作,然后给K数,i1,i2,i3.......ik 每次取走当前状态下等的第i个数(从小到大的),问取走的数的和。
给的那个的n为262144可以断定肯定是O(n*log(n))级的算法,所以选定线段树。
叶子节点赋值为1,这些节点存储的就是数的个数,在添加一个val数组记录是哪个数。每次更新将节点a[rt]变成0,找出对应的val[rt]即可。
这里坑爹的地方时sum求和时,要用—int64.wa了很多次才发现。
#include <iostream> #include <cstring> #include <cstdio> #define maxn 262145 using namespace std; int a[maxn*4],val[maxn*4]; int tmp; __int64 sum; void pushup(int rt) { a[rt] = a[rt<<1] + a[rt<<1|1]; } void build(int l,int r,int rt) { if (l == r) { a[rt] = 1; val[rt] = ++tmp; return ; } int m = (l + r)>>1; build(l,m,rt<<1); build(m + 1,r,rt<<1|1); pushup(rt); } void update(int sc,int l,int r,int rt) { if (l == r) { a[rt] = 0; sum += val[rt]; return ; } int m = (l + r)>>1; if (sc <= a[rt<<1]) update(sc,l,m,rt<<1); else { sc -= a[rt<<1]; update(sc,m + 1,r,rt<<1|1); } pushup(rt); } int main() { int n,q,t,c; int cas = 1; scanf("%d",&t); while (t--) { for (int i = 0; i < 4*maxn; ++i) a[i] = val[i] = 0; tmp = 0; scanf("%d%d",&n,&q); sum = 0; build(1,n,1); while (q--) { scanf("%d",&c); update(c,1,n,1); } printf("Case %d: %I64d\n",cas++,sum); } return 0; }
pku2828 http://poj.org/problem?id=2828
题意:N个人排队买票,每个人对应两个值pos[i] val[i]分别表示第i个人要站在第pos[i]个人之后并且其对应价值为val[i]。问给出插队序列,输出最后从前到后的价值顺序。如果具有相同的pos[i],则肯定是最后一个查到第pos[i]之后,其余的会后移,所以此题的关键就是逆序建树,然后就是插入了,注意在左子树无法满足的条件下转向右子树时记得x -= len[rt<<1];减去左边的数。
#include <iostream> #include <cstring> #include <cstdio> #include <vector> #define maxn 200001 using namespace std; int len[maxn*4]; int pos[maxn],val[maxn]; int ans[maxn]; void pushup(int rt) { len[rt] = len[rt<<1] + len[rt<<1|1]; } void build(int l,int r,int rt) { if (l == r) { len[rt] = 1; return; } int m = (l + r)>>1; build(l,m,rt<<1); build(m + 1,r,rt<<1|1); pushup(rt); } int update(int x,int l,int r,int rt) { if (l == r) { len[rt] = 0; return l; } int res = 0; int m = (l + r)>>1; if (x <= len[rt<<1]) res = update(x,l,m,rt<<1); else { x -= len[rt<<1]; res = update(x,m + 1,r,rt<<1|1); } pushup(rt); return res; } int main() { int n,i; while (~scanf("%d",&n)) { for (i = 0; i < n; ++i) scanf("%d%d",&pos[i],&val[i]); build(1,n,1); for (i = n - 1; i >= 0; --i) { int p = update(pos[i] + 1,1,n,1); ans[p] = val[i]; } for (i = 1; i < n; ++i) printf("%d ",ans[i]); printf("%d\n",ans[n]); } return 0; }
pku http://poj.org/problem?id=2886
题意:
N个小孩编号从1到n按顺时针在一个圆圈上完约瑟夫环游戏,每个人有相应的名字与val[i](表示在他之后下一个跳出圆圈的人)。每个人跳出圆圈时会得到f(p)个糖果p表示他是第几个跳出圆圈的人,f(p)表示p的约数的个数。求最大的f(p),并输出其姓名。
思路:
约瑟夫环+反素数。数据量很大模拟约瑟夫环肯定超时,所以要用n*log(n)的数据结构来解--》线段树 线段树来存储区间有多少个未跳出圆圈的点,每次计算出下一个点的相对位置,然后在线段树里面查找,并返回其绝对位置即可。
关键是计算相对位置的公式,我自己不知道怎么推出来的,希望大牛能够指点这里只能记下来了:
if (val[pos] > 0)//如果是正数,那么下一个的相对位置为 k = (k-1+val[pos]-1)%tmp+1;//tmp是记录的当前未出队列的总数 else k = ((k-1+val[pos])%tmp+tmp)%tmp+1;
然后反素数就是一个打表过程了打表函数:
反素数打表 <= 600000 #include<iostream> #include<cstring> #include <cstdio> #define maxn 600000 using namespace std; int dp[600001]; int main() { int i,j; freopen("out.txt","w",stdout); memset(dp,0,sizeof(dp)); for(i=1;i<=maxn;i++) { for(j=1;i*j<=maxn;j++) { dp[i*j]++; } } int max=0; for(i=2;i<=maxn;i++) { if(dp[i]>max) { max=dp[i]; cout<<i<<","; } } cout<<endl<<endl; max=0; for(i=2;i<=maxn;i++) { if(dp[i]>max) { max=dp[i]; cout<<dp[i]<<","; } } return 0; }
所以题解为:
#include<iostream> #include<cstring> #include <cstdio> #define maxn 500007 using namespace std; int antip[] = {1,2,4,6,12,24,36,48,60,120,180,240,360,720, 840,1260,1680,2520,5040,7560,10080,15120,20160,25200,27720, 45360,50400,55440,83160,110880,166320,221760,277200, 332640,498960,554400}; int pnum[] = {1,2,3,4,6,8,9,10,12,16,18,20,24,30,32,36,40,48, 60,64,72,80,84,90,96,100,108,120,128,144,160,168,180,192,200, 216}; int val[maxn],v[maxn*4]; char name[maxn][11]; void build(int l,int r,int rt) { v[rt] = r - l + 1;//没有pushup的形式 if (l == r) return ; int m = (l + r)>>1; build(l,m,rt<<1); build(m + 1,r,rt<<1|1); } int update(int sc,int l,int r,int rt) { v[rt]--;//没有pushup的形式 if (l == r) return l; int m = (l + r)>>1; int res; if (sc <= v[rt<<1]) res = update(sc,l,m,rt<<1); else { sc -= v[rt<<1]; res = update(sc,m + 1,r,rt<<1|1); } return res; } int main() { int n,k,i; while (~scanf("%d%d",&n,&k)) { build(1,n,1); int bsum = 0,bnum = 0; i = 0; while (antip[i] <= n) i++; bnum = antip[i - 1]; bsum = pnum[i - 1]; for (i = 1; i <= n; ++i) scanf("%s%d",name[i],&val[i]); int pos; int tmp = n; for (i = 1; i <= bnum; ++i) { pos = update(k,1,n,1); tmp--; if (tmp == 0) break; if (val[pos] > 0)//如果是正数,那么下一个的相对位置为 k = (k-1+val[pos]-1)%tmp+1;//tmp是记录的当前未出队列的总数 else k = ((k-1+val[pos])%tmp+tmp)%tmp+1; } printf("%s %d\n",name[pos],bsum); } }
上边的公式没有推出来,不过听说这道题目是经典的二分与线段数的题目,于是又做了一下。线段树存储左右区间未出圆圈的人数,相当于二分时左右区间。于是这里就好理解了,不过我认为最难理解的还是计算k的相对编号了。
if (img[l] > 0) k--;
k = ((k + img[l])%len + len)%len;
if (k == 0) k = len;
k表示的是当前出去的人,len表示的是的当前k出去之后剩余的人数,
1 2 3 4 5 假设2已经出去,当前是3出去,img[3] == 2 则下一个出去的是5,我们只要从在3+2 - 1即可得到下一个出圈人的相对编号(因为3后边的4,5原来的相对编号都-1了),所以img[l]>0时k--
如果是负数就是3 + -2了因为3前边的编号都没变,所以不用-1。还有这事1 2 3 4 不是 0 1 2 3 4所以要注意0的处理。
#include <iostream> #include <cstdio> #include <cstring> #define maxn 500007 using namespace std; int val[4*maxn]; int len,bnum,bsum,k,pos; int antip[] = {1,2,4,6,12,24,36,48,60,120,180,240,360,720, 840,1260,1680,2520,5040,7560,10080,15120,20160,25200,27720, 45360,50400,55440,83160,110880,166320,221760,277200, 332640,498960,554400}; int pnum[] = {1,2,3,4,6,8,9,10,12,16,18,20,24,30,32,36,40,48, 60,64,72,80,84,90,96,100,108,120,128,144,160,168,180,192,200, 216}; char name[maxn][12]; int img[maxn]; void build(int l,int r,int rt) { val[rt] = r - l + 1; if (l == r) return ; int m = (l + r)>>1; build(l,m,rt<<1); build(m + 1,r,rt<<1|1); } void update(int sc,int l,int r,int rt) { val[rt]--; if (l == r) { pos = l; // printf(">>%d\n",pos); if (len == 0) return; if (img[l] > 0) k--; k = ((k + img[l])%len + len)%len; if (k == 0) k = len; return ; } int m = (l + r)>>1; if (sc <= val[rt<<1]) update(sc,l,m,rt<<1); else { sc -= val[rt<<1]; update(sc,m + 1,r,rt<<1|1); } } int main() { int i,n; while (~scanf("%d%d",&n,&k)) { for (i = 1; i <= n; ++i) scanf("%s%d",name[i],&img[i]); bsum = bnum = 0; i = 0; while (antip[i] <= n) ++i; bnum = antip[i - 1]; bsum = pnum[i - 1]; build(1,n,1); len = n; for (i = 1; i <= bnum; ++i) { len--; update(k,1,n,1); } printf("%s %d\n",name[pos],bsum); } return 0; }
hdu 4302 http://acm.hdu.edu.cn/showproblem.php?pid=4302
题意:
Holedox 在一个长度为L的管道内吃蛋糕,0 y表示在y点落下蛋糕, 1表示Holedox 要吃蛋糕,Holedox 肯定吃离他最近的,如果左边没有去右边吃,如果右边没有去左边吃,如果整个管道都没有就在原地不动,如果左右两边都有,吃离他最近的,如果两边最近距离相同按照上一次的方向吃。问Holedox 经过如上操作后所行走的距离。
线段树做法:
思路:
二分区间[0,cur] [cur,0] cur表示当前位置,线段树求区间长度,以及被吃蛋糕的坐标。
#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #define maxn 100007 using namespace std; int val[4*maxn]; void pushup(int rt) { val[rt] = val[rt<<1] + val[rt<<1|1]; } void update(int pos,int sc,int l,int r,int rt) { if (l == r) { val[rt] += sc; return ; } int m = (l + r)>>1; if (pos <= m) update(pos,sc,l,m,rt<<1); else update(pos,sc,m + 1,r,rt<<1|1); pushup(rt); } int queryS(int L,int R,int l,int r,int rt) { if (l >= L && r <= R) return val[rt]; int res = 0; int m = (l + r)>>1; if (L <= m) res += queryS(L,R,l,m,rt<<1); if (R > m) res += queryS(L,R,m + 1,r,rt<<1|1); return res; } int queryP1(int sc,int l,int r,int rt) { if (l == r) return l; int res; int m = (l + r)>>1; if (sc <= val[rt<<1]) res = queryP1(sc,l,m,rt<<1); else { sc -= val[rt<<1]; res = queryP1(sc,m + 1,r,rt<<1|1); } return res; } int queryP2(int sc,int l,int r,int rt) { if (l == r) return l; int res; int m = (l + r)>>1; if (sc <= val[rt<<1|1]) res = queryP2(sc,m + 1,r,rt<<1|1); else { sc -= val[rt<<1|1]; res = queryP2(sc,l,m,rt<<1); } return res; } int main() { int t,x,y,n,q; int cas = 1; scanf("%d",&t); while (t--) { int cur = 0; int d = 1; int ans = 0; memset(val,0,sizeof(val)); scanf("%d%d",&n,&q); while (q--) { scanf("%d",&x); if (x == 0) { scanf("%d",&y); update(y,1,0,n,1);//单点更新 } else { if (val[1] == 0) continue;//不存在蛋糕 //求区间长度 int s1 = queryS(0,cur,0,n,1); int s2 = queryS(cur,n,0,n,1); //左边无蛋糕 if (!s1) { d = 1; int pos = queryP2(s2,0,n,1); ans += (pos - cur); cur = pos; update(pos,-1,0,n,1); } //右边无蛋糕 else if (!s2) { d = 0; int pos = queryP1(s1,0,n,1); ans += (cur - pos); cur = pos; update(pos,-1,0,n,1); } else { int pos1 = queryP1(s1,0,n,1); int pos2 = queryP2(s2,0,n,1); //最近点在左边 if (cur - pos1 < pos2 - cur) { d = 0; ans += (cur - pos1); cur = pos1; update(pos1,-1,0,n,1); } //在右边 else if (cur - pos1 > pos2 - cur) { d = 1; ans += (pos2 - cur); cur = pos2; update(pos2,-1,0,n,1); } else { if (d) { d = 1; ans += (pos2 - cur); cur = pos2; update(pos2,-1,0,n,1); } else { d = 0; ans += (cur - pos1); cur = pos1; update(pos1,-1,0,n,1); } } } } } printf("Case %d: %d\n",cas++,ans); } }
set做法(利用set内部红黑树log(n)的排序)
#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <set> #define maxn 100007 using namespace std; const int inf = 999999999; multiset<int>Q; int main() { int t,x,y; int n,m; int cas = 1; scanf("%d",&t); while (t--) { int cur = 0,pre = 0; int ans = 0; scanf("%d%d",&n,&m); multiset<int>::iterator it,del; Q.clear(); while (m--) { scanf("%d",&x); if (x == 0) { scanf("%d",&y); Q.insert(y); } else { if (Q.size() == 0) continue; int mi = inf; for (it = Q.begin(); it != Q.end(); ++it) { if (abs(*it - cur) < mi) { mi = abs(*it - cur); del = it; } } if (mi != inf) { for (it = Q.begin(); it != Q.end(); ++it) { if (abs(*it - cur) == mi) { if ((cur - pre)*(*it - cur) > 0) { del = it; } } } } pre = cur; cur = *del; Q.erase(del); ans += mi; } } printf("Case %d: %d\n",cas++,ans); } return 0; }
优先队列做法:利用左右两边一个出最大,一个出最小。
#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #include <set> #include <queue> #define maxn 100007 using namespace std; priority_queue<int,vector<int>,greater<int> >qmin; priority_queue<int,vector<int>,less<int> >qmax; int sum[maxn]; int main() { //freopen("4302.txt","r",stdin); int t,x,y; int n,m; int ans,cas = 1; scanf("%d",&t); while (t--) { ans = 0; int d = 1,cur = 0; while (!qmax.empty()) qmax.pop(); while (!qmin.empty()) qmin.pop(); memset(sum,0,sizeof(sum)); scanf("%d%d",&n,&m); while (m--) { scanf("%d",&x); if (x == 0) { scanf("%d",&y); sum[y]++; //注意不能把cur本身加入 if (y < cur) { if (sum[y] == 1) qmax.push(y); } else if (y > cur) { if (sum[y] == 1) qmin.push(y); } } else { if (sum[cur]) { sum[cur]--; continue; } int lsz = qmax.size(); int rsz = qmin.size(); if (lsz == 0 && rsz == 0) continue; if (lsz == 0) { d = 1; int r = qmin.top(); ans += (r - cur); cur = r; qmin.pop(); sum[cur]--; } else if (rsz == 0) { d = 0; int l = qmax.top(); ans += (cur - l); cur = l; qmax.pop(); sum[cur]--; } else { int l = qmax.top(); int r = qmin.top(); if (cur - l < r - cur) { d = 0; ans += (cur - l); cur = l; qmax.pop(); sum[cur]--; } else if (cur - l > r - cur) { d = 1; ans += (r - cur); cur = r; qmin.pop(); sum[cur]--; } else { if (d) { ans += (r - cur); cur = r; qmin.pop(); sum[cur]--; } else { ans += (cur - l); cur = l; qmax.pop(); sum[cur]--; } } } } } printf("Case %d: %d\n",cas++,ans); } return 0; }
分别对应优先队列,set,线段树。。
就做的几个题来说,单点更新有这么几种
1:节点存储区间和;2:节点存储区间最值;3:节点存储区间满足条件点的个数;