二、区间信息维护与查询

倍增、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表的简单应用

poj3264题解

poj3368 ST表进阶

poj3368题解

poj2019 二维ST表

poj2019题解

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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值