点修改
POJ 2182 Lost Cows http://poj.org/problem?id=2182
题意转换 : 有1 ~ n 编号的n头牛他们有各自的等级(没有相同的), 现在顺序被打乱了,但知道每个位置上的奶牛在他位置之前比他品牌等级高的数量,要求求出各个位置上奶牛品牌的原始排名数(等级越高,排名数越低,排名越高)。
分析 : 可以看出这是一个明显的线段树基础问题,用pre[]数组表示题目所给的每个位置上的奶牛在他位置之前比他品牌等级低的数量,ans[]数组来记录所求顺序
思路 : 从n往前退,不断更新名次来记录
例如 当pre[] = {0, 1, 2, 1, 0}, 初始可用排名编号为1,2,3,4,5
(1). 对pre[5] = 0, 说明5位置上的奶牛前没有品牌等级比他高的,所以他在可用编号中取第一个1, 此事可用编号更新为2, 3, 4, 5
(2). 同理对pre[4] = 1, 说明他在当前可用编号中等级第2个,所以取第二个即为3,可用编号更新为2, 4, 5
.....在5次更新以后获得ans[] = {2, 4, 5, 3, 1}
那哪里可以用到线段树呢?就是可用编号更新和编号选择的处理上。
下面展示代码 (细节在代码中注释)
#include<iostream>
using namespace std;
//注意poj中不能用万能头文件!!!
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i < (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, x) memset(a, x, sizeof(a))
#define el '\n'
typedef long long LL;
typedef pair<LL, LL> PLL;
typedef pair<int, int> PII;
const int MAXN = 1e4, md = 1e9 + 7, INF = 0x3f3f3f3f;
int n;
struct node{
int l, r, len;
}Tree[4 * MAXN];//通过证明可以得出需要的空间是结点数的4倍
int ans[MAXN], pre[MAXN];
void BuildTree(int l, int r, int u){ //u结点的区间范围[l, r]
Tree[u].l = l;
Tree[u].r = r;
Tree[u].len = r - l + 1;
if(l == r)//已经为叶节点 无子树
return;
//递归建立左右子树 以 (l + r) / 2为分界
BuildTree(l, (l + r) >> 1, u << 1);//位运算代替*/2会更快
BuildTree(((l + r) >> 1) + 1, r, (u << 1) + 1);
}
int query(int u, int num){//u为当前结点位置 num为在当前剩余编号中的排名,返回对应等级编号
Tree[u].len--;
if(Tree[u].l == Tree[u].r){//叶结点
return Tree[u].l;
}
if(Tree[u << 1].len < num)//左子树全部结点加起来也不够
return query((u << 1) + 1, num - Tree[u << 1].len);
else
return query(u << 1, num);
}
int main(){
cin >> n;
pre[1] = 0;//对于第一个位置来说前面一定没有其他奶牛
rep(i, 2, n){
cin >> pre[i];
}
BuildTree(1, n, 1);
dwn(i, n, 1){
ans[i] = query(1, pre[i] + 1);
}
rep(i, 1, n){
cout << ans[i] << el;
}
return 0;
}
上说我们用普通的二叉树来建立了线段树模型,那么我们是否可以用完全二叉树来建立呢?答案是可以的,并且用完全二叉树建立以后,每个结点的关系更加清晰,不再需要再通过结构体的l, r指针来找到方向,一定程度上节省了空间。
具体代码如下
#include<iostream>
#include<cmath>
using namespace std;
//注意poj中不能用万能头文件!!!
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i < (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, x) memset(a, x, sizeof(a))
#define el '\n'
typedef long long LL;
typedef pair<LL, LL> PLL;
typedef pair<int, int> PII;
const int MAXN = 1e4, md = 1e9 + 7, INF = 0x3f3f3f3f;
int n;
int ans[MAXN], pre[MAXN], Tree[4 * MAXN] = {0};//Tree只记录了长度,初始化为0
void BuildTree(int n, int last_left){ //u结点的区间范围[l, r]
lop(i, last_left, last_left + n){ //先记录叶子结点
Tree[i] = 1;
}
while(last_left != 1){//从子节点反推出父节点的长度
lop(i, last_left >> 1, last_left){
Tree[i] = Tree[i << 1] + Tree[(i << 1) + 1];
}
last_left >>= 1;
}
}
int query(int u, int num, int last_left){//u为当前结点位置 num为在当前剩余编号中的排名,返回对应等级编号
Tree[u]--;
if(Tree[u] == 0 && u >= last_left){//叶结点
return u;
}
if(Tree[u << 1] < num)//左子树全部结点加起来也不够
return query((u << 1) + 1, num - Tree[u << 1], last_left);
else
return query(u << 1, num, last_left);
}
int main(){
cin >> n;
pre[1] = 0;//对于第一个位置来说前面一定没有其他奶牛
rep(i, 2, n){
cin >> pre[i];
}
int last_left;
last_left = 1 << ((int)(log(n) / log(2)) + 1);//最底层叶节点的第一个(最左边)编号
BuildTree(n, last_left);//建树
dwn(i, n, 1){ //从n开始遍历
ans[i] = query(1, pre[i] + 1, last_left) - last_left + 1;
}
rep(i, 1, n){
cout << ans[i] << el;
}
return 0;
}
空间存储二度优化 —— 离散化处理
(以上解释出自黑书《算法竞赛入门到进阶》 罗勇军 郭卫斌)强烈推荐!!!
同样对于此题,还有一种写法,他非常简洁,并且也能达到 的效果,那就是树状数组
其主要通过树的二进制特征完成的。
核心 :
#define lowbit(x) ((x) & (-x))
//作用为找到x的二进制数的最后一个1并用这个1开始的二进制数所代表的十进制代表lowbit(x)的值
//eg 4 = (100), -4 = ~(100) + 1 = 100, lowbit(4) = (100) = 4
//(原理 : 负数的补码 = 原码取反 + 1)
long long Tree[MAXN]; //Tree[x] = m 代表的是把a[]数组的a[x - m] ~ a[x]的区间和
应用lowbit和tree[]数组实现求和与修改计算
注意 : 其实a[]数组的所有含义都隐藏在tree[]数组中,所以无需再构造a[]数组。
回到题目
1.共有n个位置,每个位置都有一头牛。所以
2.tree[]初始化当a[] 恒等于 1时,tree[i] = lowbit(i),所以这题中不需要用add()初始化
3.从最后一个位置开始,从后往前遍历n个位置设计findpos()函数,以x = findpos(pre[i] + 1)来查找所剩排名编号序列中i位置的编号。在遍历过程中每次查找完一个位置,就把这个位置改为0,即通过add(x, - 1)代表这个位置被使用,并保证能更新后续的所有tree[]。再储存ans[i] = x
具体代码和细节如下
#include<iostream>
#include<cmath>
using namespace std;
//注意poj中不能用万能头文件!!!
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i < (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, x) memset(a, x, sizeof(a))
#define el '\n'
#define lowbit(x) ((x) & (-x))
typedef long long LL;
typedef pair<LL, LL> PLL;
typedef pair<int, int> PII;
const int MAXN = 1e4, md = 1e9 + 7, INF = 0x3f3f3f3f;
int n;
int ans[MAXN], pre[MAXN], Tree[MAXN];//Tree只记录了长度,初始化为0
void add(int x, int d){//a[]的第x位加d
while(x <= n){
Tree[x] += d;
x += lowbit(x);
}
return;
}
int sum(int x){//计算a[]的前x项和
int sum = 0;
while(x > 0){
sum += Tree[x];
x -= lowbit(x);
}
return sum;
}
int findpos(int x){//采用二分查找的方法进行查找
int l = 1, r = n;
while(l < r){
int mid = (r + l) >> 1;
if(sum(mid) < x){
l = mid + 1;
}
else{
r = mid;
}
}
return l;
}
int main(){
cin >> n;
pre[1] = 0;//对于第一个位置来说前面一定没有其他奶牛
rep(i, 2, n){
cin >> pre[i];
}
rep(i, 1, n){//这题的初始化不需要用add(),因为a[]数组(代表每个位置的牛的个数)恒等于1
Tree[i] = lowbit(i);
}
dwn(i, n, 1){ //从n开始遍历
int x = findpos(pre[i] + 1);//找到对应的排名编号
add(x, -1);//把对应排名编号删除 (以后不再使用)
ans[i] = x;//记录
}
rep(i, 1, n){
cout << ans[i] << el;
}
return 0;
}
区间修改
POJ 3468 A Simple Problem with Integers
http://poj.org/problem?id=3468
这题题意非常简单,就是先给定一个初始数组长度为n,有2种操作 :
1. 把a[i] ~ a[j]所有元素都加c
2. 求a[i] ~ a[j]所有元素的区间和
共进行Q次操作。
更加分析题目的数据大致计算需要复杂度为
线段树的区间修改就正好满足这一条件,并且与这2种操作恰好融合。
核心
lazy-tag思想对一个区间如果此区间一致性(整体性)不受影响时,只更改整体和,并用tag记录(代码中的add[]函数),只有当一致性被破坏(需要把一个区间拆开)时,才把对拆开的区间计算,对拆开区间的子区间也同样用lazy-tag方法。
注意 :在一致性受破坏时候,必须把所储存的tag值先释放,再进行下一步递归。
代码如下(细节见代码注释)
#include<iostream>
#include<cstdio>
using namespace std;
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i < (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, x) memset(a, x, sizeof(a))
#define el '\n'
typedef long long LL;
typedef pair<LL, LL> PLL;
typedef pair<int, int> PII;
const int MAXN = 1e5 + 10, md = 1e9 + 7, INF = 0x3f3f3f3f;
LL sum[MAXN << 2], add[MAXN << 2];
void push_up(int rt){//向上更新,即通过下面新更新的两个子节点来更新父节点
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}
void push_down(int rt, int len){//向下更新,即通过父节点更新2个子节点来释放tag(add)值
if(add[rt]){
add[rt << 1] += add[rt];
add[rt << 1 | 1] += add[rt];
sum[rt << 1] += (len - (len >> 1)) * add[rt];//一半分到左子树
sum[rt << 1 | 1] += (len >> 1) * add[rt];//一半分到右子树
add[rt] = 0;//最后一定要归零
}
}
void BuildTree(int l, int r, int rt){
add[rt] = 0;//初始化为0
if(l == r){//递归到叶子结点(底层)
scanf("%lld", &sum[rt]);
return;
}
int mid = (l + r) >> 1;
BuildTree(l, mid, rt << 1);
BuildTree(mid + 1, r, rt << 1 | 1);
push_up(rt);//从底层向上更新
}
void updata(int a, int b, LL c, int l, int r, int rt){//[a,b]区间所有增加c
if(a <= l && b >= r){//区间包含情况直接lazy
sum[rt] += (r - l + 1) * c;
add[rt] += c;
return;
}
push_down(rt, r - l + 1);//向下更新先释放tag
int mid = (r + l) >> 1;
if(a <= mid)
updata(a, b, c, l, mid, rt << 1);
if(b > mid)
updata(a, b, c, mid + 1, r, rt << 1 | 1);
push_up(rt);//再回头向上更新sum
}
LL query(int a, int b, int l, int r, int rt){//查询[a,b]区间和
if(a <= l && b >= r){
return sum[rt];
}
push_down(rt, r - l + 1);//向下释放tag
int mid = (r + l) >> 1;
LL ans = 0;
if(a <= mid)
ans += query(a, b, l, mid, rt << 1);
if(b >mid)
ans += query(a, b, mid + 1, r, rt << 1 | 1);
return ans;
}
int main(){
int n, Q;
cin >> n >> Q;
BuildTree(1, n, 1);//[1, N]区间结点从1开始编号(1为根节点)
while(Q--){
string opt;
int a, b;
LL c;
cin >> opt;
//cout << opt << el;
if(opt == "Q"){//查询
scanf("%d %d", &a, &b);
//cout << a << b << el;
cout << query(a, b, 1, n, 1) << el;
}
else{//更新
scanf("%d %d %lld", &a, &b, &c);
//cout << a << b << el << el;
updata(a, b, c, 1, n, 1);
}
}
}
特别提醒这题中用cin和cout可能会超时,建议使用scanf和printf进行,并且不开long long,必寄!