倍增、ST、RMQ
- 倍增
顾名思义,就是成倍地增加
对于规模特别大的问题,可以每次只查找2的整数次幂位置,可以快速缩小所要找的范围。
原理是任意一个正整数都可以分解成几个2的整数次幂的和。
- ST(稀疏表)
一般用于查询区间的最值,预处理2的整数次幂长度的区间最值。
如果我们要查找的区间长度不是2的整数次幂,我们可以取两段稍小于其长度的区间,这个区间的长度是2的整数次幂,然后取二者的最值。
比如要查[a,a+k]区间的最值,设len是不大于k的最大2的整数次幂,取两段头尾分别是a和a+k的长度为len的区间的最值即可。
- RMQ
区间最值查询,常用方法有ST、线段树、树状数组等。
ST的优点是查询时间快O(1),但是不支持修改
线段树和树状数组的查询时间为O(logn),但是支持修改
poj3264 区间最值 ST表的简单应用
poj3368 ST表进阶
poj2019 二维ST表
LCA 最近公共祖先
两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。
以LCA(u,v)表示u和v的最近公共祖先
- 暴力算法
先将深度大的点向上调整至和小的点相同,然后两个点同时向上移动,直到两个点移动到同一个点
如果这棵树建的比较平衡,用这种方法的时间复杂度为O(logn),但是遇到一条链特别长的情况时,复杂度可能会退化到O(n)
- 树上倍增算法
用F[i][j]来表示i的第2^j个父节点。
先将深度大的点向上调整至与小的点相同,然后从最大的 j 开始不断尝试,直到第一次出现 F[u][j] != F[v][j],令u = F[u][j],v = F[v][j],因为LCA(F[u][j],F[v][j]) = LCA(u,v)。然后继续循环直到u == v
即使在极端条件下时间复杂度也为O(logn)
- 在线RMQ算法
欧拉序列:在深度遍历的时候将依次经过的结点记录下来,回溯时也将经过的结点记录下来
比如这里有一棵树,从1开始dfs,得到的欧拉序列为1 2 4 6 8 6 9 6 4 2 5 7 5 2 1 3 1如果要找 5 和 6 的最近公共祖先,首先找到 5 和 6 在欧拉序列中首次出现的位置,这个区间中深度最小的结点就是他们的公共祖先。
可以对欧拉序列建立ST表加速区间最值的查询
- Tarjan离线算法
离线算法是指读入所有询问,运行一次得到所有结果
Tarjan算法利用了并查集的时空优越性,可以在O(n+m)时间内解决LCA问题
查询过程:由于用到了并查集,需要开辟两个数组,fa[],vis[],初始化fa[i]=i,vis[]=0
从根节点1开始dfs,标记vis[1]=1,然后遍历1的子树,将沿途遍历到的结点的vis置1
当一个结点的子树全部都遍历完了,更新这个结点的fa为其父结点,查看是否有关这个结点的询问,如果没有,回退到父结点,如果有,查看另一个结点的vis是否为1,如果不是,依然回退到父结点,否则从另一结点利用并查集向上查找LCA(找到某个点的fa等于自己的时候停下,这个点就是LCA),在找到LCA后将沿途经过的结点fa赋值为LCA。
不断重复这个过程就可以完成所有的查询。
poj1330 简单LCA应用
这题非常简单,就不写题解了
主要实现代码
int fa[N], vis[N]; //fa记录父结点,vis记录是否访问
while (x != 0)
{
vis[x] = 1;
x = fa[x];
}
while (y != 0)
{
if (vis[y])
{
printf("%d\n", y);
break;
}
y = fa[y];
}
poj1986 tarjan离线算法模板题
这题的方向其实是不影响结果的,输入后不管就行了。
#include <cstdio>
#include <cstring>
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
inline void scf(int &a) { scanf("%d", &a); }
inline void scf(int &a, int &b) { scanf("%d%d", &a, &b); }
const int N = 1e6 + 5;
const int M = 1e6 + 5;
using namespace std;
//采用链式前向星方式存边
struct edge
{
int to; //与之连接的点的编号
int dist; //边长
int next; //另一条边的下标
} e[M], qe[M];
int head[N], qhead[N], cnt, qcnt;
int fa[N], dis[N]; // dis[i]表示结点i到根结点的距离
bool vis[N];
void init() //初始化
{
mem(vis, 0);
cnt = qcnt = dis[1] = 0;
for (int i = 0; i < N; i++)
{
head[i] = qhead[i] = -1;
fa[i] = i;
}
}
void addedge(int u, int v, int dist) //农村加边
{
e[cnt].to = v;
e[cnt].dist = dist;
e[cnt].next = head[u];
head[u] = cnt;
cnt++;
//因为是无向图所以要加两条边
e[cnt].to = u;
e[cnt].dist = dist;
e[cnt].next = head[v];
head[v] = cnt;
cnt++;
}
//由于一个点可能存在多个查询,因此也用前向星来保存查询
void addqedge(int u, int v) //询问加边
{
qe[qcnt].to = v;
qe[qcnt].next = qhead[u];
qhead[u] = qcnt;
qcnt++;
//因为是无向图所以要加两条边
qe[qcnt].to = u;
qe[qcnt].next = qhead[v];
qhead[v] = qcnt;
qcnt++;
}
//利用并查集找tarjan
int Find(int x)
{
if (x != fa[x]) fa[x] = Find(fa[x]);
return fa[x];
}
void tarjan(int u)
{
fa[u] = u;
vis[u] = true;
//遍历子树
for (int i = head[u]; i != -1; i = e[i].next)
{
int v = e[i].to;
if (!vis[v])
{
dis[v] = dis[u] + e[i].dist;
tarjan(v);
fa[v] = u;
}
}
//当点u的所有子树都遍历完之后查看点u是否有查询
for (int i = qhead[u]; i != -1; i = qe[i].next)
{
int v = qe[i].to;
if (vis[v])
{
//两个点的距离 = 二者到根结点的距离之和 - LCA到根结点距离的两倍
qe[i].dist = dis[u] + dis[v] - 2 * dis[Find(v)];
// i^1表示v到u的边
qe[i ^ 1].dist = qe[i].dist;
}
}
}
int main()
{
init();
int n, m, u, v, w, q;
char c;
scf(n, m);
getchar();
while (m--)
{
scanf("%d %d %d %c", &u, &v, &w, &c);
getchar();
addedge(u, v, w);
}
scf(q);
while (q--)
{
scf(u, v);
addqedge(u, v);
}
tarjan(1);
//加边的顺序和输入的顺序相同,因此将qe的dist按顺序输出即可
for (int i = 0; i < qcnt; i += 2)
printf("%d\n", qe[i].dist);
return 0;
}
一维树状数组
原理
普通的前缀和查询效率很高,为O(1),但是修改的时间为O(n),因为前缀和在修改的时候连带着整段区间都要修改。
树状数组是一种用空间换时间的做法,为了减少修改量,树状数组的做法是将一个区间拆分成多个,分别进行维护,因此每次修改的时候只要修改相应要维护的区间即可,这种做法在空间上比前缀和增加了近一倍,但是修改复杂度降到了O(logn),查询复杂度也上升到了O(logn),对于需要频繁修改的情况可以节省不少时间。
具体做法
A数组为原始数据
树状数组引入了管理数组C,C[i]维护着A[i]和若干个C[j](j<i)的和。
维护的规则是i的二进制表示下末尾有k个0,则C[i]维护着A[i-2k +1]~A[i]的和,也就是区间长度为2k举个例子:
6的二进制是110 末尾有1个0 则C[6] = A[6] + A[5]
3的二进制是11 末尾没有0 则C[3] = A[3]为了快速得到区间的长度,可以将i取反然后和i相与 就可以得到i的最低位的1和后面的0组成的数了,这个就是区间长度,因此树状数组里有一个lowbit函数。
int lowbit(int i) { return (-i) & i; }
前驱:C[i]的直接前驱为C[i-lowbit(i)],那么求前缀和只要将当前点的以及所有前缀点的C相加即可
后继:C[i]的直接后继为C[i+lowbit(i)],那么修改一个点只要修改当前点和所有后继点即可
int sum(int i) //查询前i个元素的和
{
int res = 0;
while (i > 0)
{
res += C[i];
i -= lowbit(i);
}
return res;
}
int sum(int i, int j) { return sum(j) - sum(i - 1); } //查询区间和
void add(int i, int x) //将i点加上x
{
while (i <= n)
{
C[i] += x;
i += lowbit(i);
}
}
应用场景: 点修改、区间查询、前缀和
poj2352 树状数组入门题
因为这题的输入是按照y坐标升序来的,当y相同的时候是按x升序来的,因此后续输入的点不会对前面的点造成影响,直接当作一维来做即可。
#include <cstdio>
#include <cstring>
#define mem(a, v) memset(a, v, sizeof(a))
const int N = 32005;
using namespace std;
int c[N], ans[N];
inline int lowbit(int i) { return (-i) & i; }
void add(int x)
{
while (x < N)
{
c[x]++;
x += lowbit(x);
}
}
int sum(int x)
{
int res = 0;
while (x > 0)
{
res += c[x];
x -= lowbit(x);
}
return res;
}
int main()
{
int n, x, y;
mem(ans, 0);
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
scanf("%d%d", &x, &y);
x++; //将x++ 防止x为0的情况
ans[sum(x)]++;
add(x);
}
for (int i = 0; i < n; i++)
printf("%d\n", ans[i]);
return 0;
}
POJ3067 公路交叉问题 树状数组求逆序数对
两条公路没有交叉的条件是x1<x2 && y1<y2 或者 x1>x2 && y1>y2 如果不符合这个条件就会存在交叉(x1 = x2 或 y1 = y2也算交叉)
因此将输入的数对按x升序排列,x相同的时候按y升序排列,这样后面的数对在更新的时候就不会对前面的结果产生影响。然后用一个树状数组来维护y,按顺序处理数对的时候只要查询大于等于当前数对的y的数量即可。
需要注意的是本题的结果会非常大,需要用long long来存储。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
inline void scf(int &a, int &b) { scanf("%d%d", &a, &b); }
const int N = 1e3 + 5;
using namespace std;
struct edge
{
int u, v;
} e[N * N];
bool cmp(edge a, edge b)
{
if (a.u != b.u) return a.u < b.u;
return a.v < b.v;
}
int c[N], T, n, m, k;
inline int lowbit(int x) { return (-x) & x; }
inline void add(int i)
{
while (i <= m)
{
c[i]++;
i += lowbit(i);
}
}
inline int sum(int i)
{
int res = 0;
while (i > 0)
{
res += c[i];
i -= lowbit(i);
}
return res;
}
int main()
{
cin >> T;
for (int t = 1; t <= T; t++)
{
long long ans = 0;
mem(c, 0);
cin >> n >> m >> k;
for (int i = 0; i < k; i++)
scf(e[i].u, e[i].v);
sort(e, e + k, cmp);
for (int i = 0; i < k; i++)
{
//查询v值位于[e[i].v, m]区间的公路条数
ans += i - sum(e[i].v);
add(e[i].v);
}
printf("Test case %d: %lld\n", t, ans);
}
return 0;
}
poj3321 利用DFS将树转换为序列
将一棵树进行深度优先遍历,记录遍历时当前节点进入和出去时的编号,两个序号之间的节点就是当前节点的子节点。
那么这题的树转换成序列后就可以用树状数组来维护区间和了。
#include <cstdio>
#include <cstring>
#include <vector>
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
const int N = 1e5 + 5;
using namespace std;
//用邻接表可能会超时,建议用链式前向星来存储边
struct edge
{
int u, next;
} e[N];
int head[N], cnt = 0;
int n, dfn, l[N], r[N], c[N], a[N];
int lowbit(int i) { return (-i) & i; }
void add(int x, int v)
{
while (x <= n)
{
c[x] += v;
x += lowbit(x);
}
}
int sum(int x)
{
int res = 0;
while (x > 0)
{
res += c[x];
x -= lowbit(x);
}
return res;
}
void dfs(int x)
{
l[x] = ++dfn;
for (int i = head[x]; i; i = e[i].next)
dfs(e[i].u);
r[x] = dfn;
}
int main()
{
while (~scanf("%d", &n))
{
int x, y, q;
char op[12];
dfn = 0;
for (int i = 1; i <= n; i++)
{
add(i, 1);
a[i] = 1;
head[i] = 0;
}
for (int i = 1; i < n; i++)
{
scanf("%d%d", &x, &y);
e[++cnt].u = y;
e[cnt].next = head[x];
head[x] = cnt;
}
dfs(1);
scanf("%d", &q);
while (q--)
{
scanf("%s%d", &op, &x);
if (op[0] == 'C')
{
if (a[l[x]])
add(l[x], -1), a[l[x]] = 0;
else
add(l[x], 1), a[l[x]] = 1;
}
else
printf("%d\n", sum(r[x]) - sum(l[x] - 1));
}
}
return 0;
}
多维树状数组
以二维树状数组为例,只要多一层循环即可:
int sum(int x, int y)
{
int res = 0;
for (int i = x; i > 0; i -= lowbit(i))
for (int j = y; j > 0; j -= lowbit(j))
res += C[i][j];
return res;
}
int sum(int x1, int y1, int x2, int y2) { return sum(x2, y2) - sum(x1 - 1, y2) - sum(x2, y1 - 1) + sum(x1 - 1, y2 - 1); }
void add(int x, int y, int w)
{
for (int i = x; i <= n; i += lowbit(i))
for (int j = y; j <= n; j += lowbit(j))
C[i][j] += w;
}
poj1195 多维树状数组简单应用
#include <cstdio>
#include <cstring>
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
const int N = 1e3 + 30;
using namespace std;
int c[N][N], n;
inline int lowbit(int x) { return x & -x; }
inline void add(int x, int y, int val)
{
for (int i = x; i <= n; i += lowbit(i))
for (int j = y; j <= n; j += lowbit(j))
c[i][j] += val;
}
inline int sum(int x, int y)
{
int res = 0;
for (int i = x; i > 0; i -= lowbit(i))
for (int j = y; j > 0; j -= lowbit(j))
res += c[i][j];
return res;
}
inline int sum(int x1, int y1, int x2, int y2) { return sum(x2, y2) - sum(x1 - 1, y2) - sum(x2, y1 - 1) + sum(x1 - 1, y1 - 1); }
int main()
{
mem(c, 0);
int op, x1, x2, y1, y2, s;
scanf("%d%d", &op, &s);
n = s;
while (1)
{
scanf("%d", &op);
if (op == 3) break;
if (op == 1)
{
scanf("%d%d%d", &x1, &y1, &s);
add(x1 + 1, y1 + 1, s); //因为下标有可能出现0 因此传参的时候加1
}
else
{
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", sum(x1 + 1, y1 + 1, x2 + 1, y2 + 1)); //因为下标有可能出现0 因此传参的时候加1
}
}
return 0;
}
线段树
线段树可以认为是树状数组的加强版,可以完成所有树状数组的功能,同时支持区间修改。
线段树主要由四大函数构成:build、pushdown、update、query。
关于lazy标记:
lazy标记的思想与并查集的路径压缩有相同之处,就是用到这个结点的时候再更新,否则就不管,以减少更新量。
如果要修改的区间完全包含当前结点的区间,只要对这个结点打一个标记就行,而不用将下面所有的结点一并修改。如果下次查询到这个结点的子树时,将lazy标志下传到子树即可,因为lazy标志只影响子树,对父结点没有影响,所以只要把lazy标志下传到要查询的结点位置就可以避免其影响。
因此,在区间修改和区间查询的时候,进入子树之前需要先进行pushdown操作。
poj3468 简单线段树应用 可以直接当模板用
我这里用结构体来定义一个结点以及其维护的区间,这样写的话几个操作函数的代码量比较小,而且更直观。如果题目对空间要求比较高的话,可以在函数中定义区间(传参时多传一个当前结点的左右边界)。
//TIME:2578ms
#include <cstdio>
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
const int N = 1e5 + 5;
using namespace std;
struct node
{
int l, r; //这个结点维护区间[l,r]的和
long long val, lazy; // lazy标记表示子树的区间中每个点要加lazy
node() { val = lazy = 0; }
} tree[N * 4]; //理想情况下需要N*2的结点数量,但是开四倍大小是最保险的
long long a[N];
//递归建树,i为结点下标
void bulid(int l, int r, int i = 1)
{
tree[i].l = l;
tree[i].r = r;
if (l == r)
tree[i].val = a[l];
else
{
int mid = (l + r) >> 1;
//建左子树
bulid(l, mid, i << 1);
//建右子树
bulid(mid + 1, r, i << 1 | 1);
//子树更新后更新父节点
tree[i].val = tree[i << 1].val + tree[i << 1 | 1].val;
}
}
// lazy标记下传
void pushdown(int i)
{
if (tree[i].lazy) //如果这个结点有标记就下传
{
//子树加上lazy标记
tree[i << 1].lazy += tree[i].lazy;
tree[i << 1 | 1].lazy += tree[i].lazy;
//因为lazy标记是针对子树是否要更新而言的(与当前结点的val无关),所以lazy下传的时候子树的val需要更新,当前结点的val无需更新
tree[i << 1].val += (tree[i << 1].r - tree[i << 1].l + 1) * tree[i].lazy;
tree[i << 1 | 1].val += (tree[i << 1 | 1].r - tree[i << 1 | 1].l + 1) * tree[i].lazy;
//删除当前结点的lazy标记
tree[i].lazy = 0;
}
}
//更新[l,r]区间
void update(int l, int r, int val, int i = 1)
{
//如果当前结点所维护的区间都在要更新的区间内就直接更新
if (tree[i].l >= l && tree[i].r <= r)
{
tree[i].val += (tree[i].r - tree[i].l + 1) * val;
//子树未更新,给当前结点添加lazy标记
tree[i].lazy += val;
return;
}
//如果要更新子树的话先把lazy标记下传
pushdown(i);
int mid = (tree[i].l + tree[i].r) >> 1;
//检查左子树是否要更新,l <= mid说明左子树维护的区间有一部分也在要更新的区间内
if (l <= mid) update(l, r, val, i << 1);
//同上
if (r > mid) update(l, r, val, i << 1 | 1);
//子树更新后更新父结点
tree[i].val = tree[i << 1].val + tree[i << 1 | 1].val;
}
//查询[l,r]区间的和
long long query(int l, int r, int i = 1)
{
if (tree[i].l >= l && tree[i].r <= r) return tree[i].val;
pushdown(i);
int mid = (tree[i].l + tree[i].r) >> 1;
//如果查询区间只在左子树内就去左子树
if (r <= mid) return query(l, r, i << 1);
//只在右子树内
if (l > mid) return query(l, r, i << 1 | 1);
//左右子树都包含查询区间
return query(l, mid, i << 1) + query(mid + 1, r, i << 1 | 1);
}
int main()
{
int n, m, x, y;
long long val;
char op[10];
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
bulid(1, n);
while (m--)
{
scanf("%s", op);
if (op[0] == 'Q')
{
scanf("%d%d", &x, &y);
printf("%lld\n", query(x, y));
}
else
{
scanf("%d%d%lld", &x, &y, &val);
update(x, y, val);
}
}
return 0;
}
poj2777 颜色统计
这题不求区间和,改求区间的颜色数量,因为颜色最多就30种,可以设置一个标记数组来记录某个颜色是否出现过,最后遍历一遍就可得到颜色数量了。
本题的lazy标记和之前的用法有点不同,用来标记这个区间的颜色是否都相同。
#include <cstdio>
#include <cstring>
#define mem(a, v) memset(a, v, sizeof(a))
#define fre(f) freopen(f ".in", "r", stdin), freopen(f ".out", "w", stdout)
const int N = 1e5 + 5;
using namespace std;
bool vis[31];
int color[N * 4]; // 相当于lazy标记,color[i]为0的时候说明该区间可能存在多个颜色,不为0的时候表示这个区间的颜色都是color[i]
inline int countcolor(int t)
{
int res = 0;
for (int i = 1; i <= t; i++)
if (vis[i]) res++;
return res;
}
inline void pushdown(int rt)
{
if (color[rt])
{
color[rt << 1] = color[rt];
color[rt << 1 | 1] = color[rt];
color[rt] = 0; //标记为0表示该区间可能存在多个颜色
}
}
void modify(int l, int r, int c, int L, int R, int rt = 1)
{
if (L >= l && R <= r)
color[rt] = c;
else
{
pushdown(rt);
int mid = (L + R) / 2;
if (l > mid)
modify(l, r, c, mid + 1, R, rt << 1 | 1);
else if (r <= mid)
modify(l, r, c, L, mid, rt << 1);
else
modify(l, r, c, L, mid, rt << 1), modify(l, r, c, mid + 1, R, rt << 1 | 1);
}
}
void query(int l, int r, int L, int R, int rt = 1)
{
//如果当前区间只有一种颜色,直接返回
//否则还要进入左右子树查询颜色
if (color[rt])
{
vis[color[rt]] = 1;
return;
}
int mid = (L + R) / 2;
if (l > mid)
query(l, r, mid + 1, R, rt << 1 | 1);
else if (r <= mid)
query(l, r, L, mid, rt << 1);
else
query(l, r, L, mid, rt << 1), query(l, r, mid + 1, R, rt << 1 | 1);
}
int main()
{
// fre("2777");
int l, t, o, x, y, c;
char op[10];
color[1] = 1;
scanf("%d%d%d", &l, &t, &o);
while (o--)
{
scanf("%s%d%d", op, &x, &y);
if (op[0] == 'C')
{
scanf("%d", &c);
modify(x, y, c, 1, l);
}
else if (op[0] == 'P')
{
mem(vis, 0);
query(x, y, 1, l);
printf("%d\n", countcolor(t));
}
}
return 0;
}
分块
线段树能解决的问题一般要满足区间合并性(比如可加可减),对于一些线段树解决不了的问题可以采用分块的方法,分块实质上就是优化后的暴力算法。采用的是大段维护,小段暴力的做法,将维护的数据分成若干个块,记录每个数据所位于的块位置,为了分块数和分块长度的平衡性,一般取分块长度k为
n
2
\sqrt[2]n
2n。如果n不是平方数,那么最后一个块的长度会小于k。
分块在查询和修改的效率上要稍差于线段树,但是实现起来要比线段树要简单很多,也更容易理解。
用到的数组有
- L[i] 表示第i个分块的左端
- R[i]表示第i个分块的右端
- pos[i]表示第i个数据位于的分块编号
- sum[i]第i个区间的和(实际使用中不一定是求和,也可以是其他操作)
- lazy[i]第i个区间的懒操作,参考线段树
修改操作
假如要修改区间[i,j],如果i和j位于同一个分块,那么就暴力更新,否则中间的分块进行懒操作,两端的分块进行暴力更新。
查询操作
假如要查询的区间为[i,j],如果i和j位于同一个分块,那么就暴力求和,否则中间的分块直接用sum来求,两端的分块进行暴力求和。进行暴力求和时还得加上当前分块的懒标记。
poj3468分块解法
采用线段树的做法用了2579ms,可以看到分块在时间上并不会差很多,而且比线段树要更好写。
// TIME:2704ms
#include <algorithm>
#include <cmath>
#include <cstdio>
const int N = 1e5 + 5;
using namespace std;
int pos[N], L[N], R[N];
long long a[N], sum[N], add[N];
int main()
{
int n, q, k, len;
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
k = sqrt(n);
len = n / k;
if (n % k) len++;
for (int i = 1; i <= len; i++)
{
add[i] = 0;
L[i] = (i - 1) * k + 1;
R[i] = min(i * k, n);
for (int j = L[i]; j <= R[i]; j++)
{
sum[i] += a[j];
pos[j] = i;
}
}
while (q--)
{
char c;
int l, r, lp, rp;
long long v;
getchar();
scanf("%c%d%d", &c, &l, &r);
lp = pos[l];
rp = pos[r];
if (c == 'Q')
{
long long ans = 0;
if (lp == rp)
{
for (int i = l; i <= r; i++)
ans += a[i];
ans += (r - l + 1) * add[lp]; //加上懒标记
}
else
{
for (int i = l; i <= R[lp]; i++)
ans += a[i];
ans += (R[lp] - l + 1) * add[lp]; //加上懒标记
for (int i = L[rp]; i <= r; i++)
ans += a[i];
ans += (r - L[rp] + 1) * add[rp]; //加上懒标记
for (int i = lp + 1; i < rp; i++)
ans += sum[i];
}
printf("%lld\n", ans);
}
else
{
scanf("%lld", &v);
if (lp == rp)
{
for (int i = l; i <= r; i++)
a[i] += v;
sum[lp] += v * (r - l + 1);
}
else
{
for (int i = l; i <= R[lp]; i++)
a[i] += v;
sum[lp] += v * (R[lp] - l + 1);
for (int i = L[rp]; i <= r; i++)
a[i] += v;
sum[rp] += v * (r - L[rp] + 1);
for (int i = lp + 1; i < rp; i++)
{
sum[i] += (R[i] - L[i] + 1) * v;
add[i] += v;
}
}
}
}
return 0;
}
poj1019 分块问题变形
题目的对所给的序列描述不是很清楚,这里用一串代码来解释一下:
输出的结果就是题目中所给的序列了,题目要我们输出这个序列中第n位数字
for (int i = 1; ; i++)
for (int j = 1; j <= i; j++)
cout << j;
这题将每一次 i 的循环输出的序列作为一个分块,因此并不是所有的分块都是一样的长度。将所有的数字都存储下来是不合理的,不管是时间还是空间上,只要记录每个分块的长度即可。对于每次查询,先找到分块的位置,然后从分块中找到数字。
所以我们可以用一个数组len[i]来记录每一个分块的长度,这样便于我们定位到查询位置所在的分块。
#include <cmath>
#include <iostream>
const int N = 4e5 + 5;
using namespace std;
int len[N];
int main()
{
len[1] = 1;
long long sum = 1;
for (int i = 2; sum < 2147483647; i++)
{
len[i] = len[i - 1] + (int)(log10(i) + 1);
sum += len[i];
}
int t;
cin >> t;
while (t--)
{
int k;
cin >> k;
//找到k位于的分块位置
for (int i = 1; k - len[i] > 0; i++)
k -= len[i];
//找到分块中的位置
for (int i = 1;; i++)
{
k -= (int)(log10(i) + 1);
if (k <= 0)
{
k = -k; //取反后就是i要除k次10,之后的最低位就是要输出的数字
while (k--)
i /= 10;
cout << i % 10 << endl;
break;
}
}
}
return 0;
}