树状数组和线段树的应用

线段树的一些技巧

单点修改的写法

有单点修改操作时,使用map记录数组某个位置的点在线段树中的位置可以更快更好写地修改。

void build(int l, int r, int rt)
{
    if (l == r)
    {
        //...
        map[l] = rt;
        return;
    }
    //...
}
void update(int rt, int v)
{
    rt = map[rt];
    tree[rt] += v;
    while (rt >>= 1)
        update(rt);
}

一些封装思想

传统面向过程的线段树在维护复杂信息时可能有很多不便,把一些lazytag相关操作封装起来比如:

pushdown(rt)
init_lazy(rt) //初始化/还原lazy
cal_lazy(rt) //计算lazy对当前节点的影响
union_lazy(rt, son) //传递标记

之后所有线段树题直接套板子,只需要重新设计lazytag就好了

在E题疯狂WA之后被教做人了。。改了波码风怎么就A了(躺)

动态DP

Dynamic Dynamic Progarmming

本质上是线段树维护转移矩阵。

由DAG引入,假设现在有这个图(行表示起点列表示终点):
$$
对其中矩阵乘法进行扩展:

矩阵中对乘法的operator这里: tmp[i][j] = a[i][k] * a[k][j] 改成 tmp[i][j] = min(a[i][k] + a[k][j], tmp[i][j]) 就变成了Floyd。

然后把这个矩阵放到线段树中维护,就可以做DAG上的带修Flyod。

题目

K 矩阵表示

题意

上场E题传送门

在此基础上增加单点修改,即某次操作可能会修改某个中间楼梯的方向。

思路

显然可以树状数组维护前缀和,但不如线段树更优美。

因为树状数组需要矩阵求逆,但线段树只需要考虑区间合并(仅矩阵乘法)。

代码

#include <cstdio>
#include <iostream>
#define rep(i,s,t) for(int i=s;i<=t;i++)
#define root 1,n,1
#define mid (l+r)>>1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
using namespace std;
using ll = long long;
const int MAXN = 1e5 + 233;
const ll MOD = 1e9 + 7;
int n, M;
struct Matrix
{
  ll a[2][2];
  Matrix()
  {
    a[0][0] = a[1][1] = 1;
    a[1][0] = a[0][1] = 0;
  }
  Matrix(ll a00, ll a01, ll a10, ll a11)
  {
    a[0][0] = a00;
    a[0][1] = a01;
    a[1][0] = a10;
    a[1][1] = a11;
  }
  Matrix operator * (const Matrix &B)
  {
    Matrix C(0, 0, 0, 0);
    rep (i, 0, 1)
      rep (j, 0, 1)
        rep (k, 0, 1)
          C.a[i][j] = (C.a[i][j] + a[i][k] * B.a[k][j]) % MOD;
    return C;
  }
} mtx[MAXN];
struct Tnode
{
  Matrix sum;
};
struct Tree
{
  Tnode t[MAXN << 2];
  int mp[MAXN]; //树节点,与数组的映射
  void update(int rt)
  {
    t[rt].sum = t[rt << 1].sum * t[rt << 1 | 1].sum;
  }
  void build(int l, int r, int rt)
  {
    if (l == r)
    {
      t[rt].sum = mtx[l];
      mp[l] = rt;
      return;
    }
    int m = mid;
    build(lson);
    build(rson);
    update(rt);
  }
  void modify(int p, Matrix v)
  {
    int rt = mp[p];
    t[rt].sum = v;
    while (rt >>= 1)
      update(rt);
  }
  Matrix query(int l, int r, int rt, int nl, int nr)
  {
    if (nl <= l && r <= nr)
    {
      return t[rt].sum;
    }
    int m = mid;
    if (nl <= m)
      if (m < nr)
        return query(lson, nl, nr) * query(rson, nl, nr);
      else
        return query(lson, nl, nr);
    else
      return query(rson, nl, nr);
  }
} ST;
int main()
{
  //freopen("in.txt", "r", stdin);
  scanf("%d%d ", &n, &M);
  char ch;
  rep (i, 2, n) //写法有关,这里记录的是到达这层的楼梯
  {
    scanf("%c", &ch);
    if (ch == '/')
      mtx[i] = Matrix(1, 1, 0, 1);
    else
      mtx[i] = Matrix(1, 0, 1, 1);
  }
  ST.build(root);
  int opt, l, r, s, t;
  Matrix x;
  while (M--)
  {
    scanf("%d", &opt);
    if (!opt)
    {
      scanf("%d %c", &l, &ch);
      if (ch == '/')
        x = Matrix(1, 1, 0, 1);
      else
        x = Matrix(1, 0, 1, 1);
      ST.modify(l + 1, x);
    }
    else
    {
      scanf("%d%d%d%d", &l, &r, &s, &t);
      printf("%lld\n", ST.query(root, l + 1, r).a[s][t]);
    }
  }
  return 0;
}

J DDP

题意

在原题的基础上添加了:

  • 像K题一样可以修改中间楼梯的结构(单点)。

  • 每个楼梯都有权值,走过有一定花费,可以进行修改(单点)。

  • 查询最短路,而不是方案数。

思路

类似最短路,矩阵里的每个数字改为表示权值(没有楼梯就置为inf)。

在矩阵乘法中做一些修改,相乘改成类似Floyd的求min操作。

用线段树维护即可。

代码

#include <cstdio>
#include <iostream>
#include <cstring>
#define rep(i,s,t) for(int i=s;i<=t;i++)
#define root 1,n,1
#define mid (l+r)>>1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
using namespace std;
using ll = long long;
const int MAXN = 1e5 + 233;
const ll INF = 1e15 + 233;
int n, M;
struct Matrix
{
  //矩阵除了对乘法的operator以外与上题完全一致
  Matrix operator * (const Matrix &B)
  {
    Matrix C(INF, INF, INF, INF);
    rep (i, 0, 1)
      rep (j, 0, 1)
        rep (k, 0, 1)
          C.a[i][j] = min(C.a[i][j], a[i][k] + B.a[k][j]); //改成类似floyd的做法
    return C;
  }
} mtx[MAXN];
struct Tnode
{
  Matrix sum;
};
struct Tree
{
    //线段树部分与上题完全一致
} ST;
char str[MAXN];
int main()
{
  freopen("in.txt", "r", stdin);
  scanf("%d%d ", &n, &M);
  scanf("%s", str + 2);
  int v0, v1, v2;
  rep (i, 2, n)
  {
    scanf("%d%d%d", &v0, &v1, &v2);
    if (str[i] == '/')
      mtx[i] = Matrix(v0, v2, INF, v1); //跟图论题一样的初始化emm
    else
      mtx[i] = Matrix(v0, INF, v2, v1);
  }
  ST.build(root);
  int opt, l, r, s, t;
  char ch;
  Matrix x;
  while (M--)
  {
    scanf("%d", &opt);
    if (opt == 0)
    {
      scanf("%d %c", &l, &ch);
      char nowch = mtx[l + 1].a[1][0] == INF ? '/' : '\\';
      if (ch != nowch)
      {
        swap(mtx[l + 1].a[0][1], mtx[l + 1].a[1][0]);
        ST.modify(l + 1, mtx[l + 1]);
      }
    }
    else if (opt == 1)
    {
      scanf("%d", &l);
      scanf("%d%d%d", &mtx[l + 1].a[0][0], &mtx[l + 1].a[1][1],
        (mtx[l + 1].a[1][0] == INF) ? &mtx[l + 1].a[0][1] : &mtx[l + 1].a[1][0]);
      ST.modify(l + 1, mtx[l + 1]);
    }
    else
    {
      scanf("%d%d%d%d", &l, &r, &s, &t);
      Matrix ans = ST.query(root, l + 1, r);
      printf("%lld\n", ans.a[s][t] >= INF ? -1 : ans.a[s][t]);
    }
  }
  return 0;
}

H 线段树的二分思想

题意

给出一堆箱子(),每个箱子最多放大小(),有一些物品()序列,每个物品有大小(),每个箱子不能超过个()物品。

每次从左到右寻找能放下当前物品的箱子并放下,询问放在哪里。

思路

二分思想,线段树维护区间max。

每次按照以下流程:

  • 根节点能不能放下,放不下直接输出"-1"。
  • 看左右子树能不能放下,按题意优先左子树。
  • 一直到叶子节点,进行单点加操作,再一路合并上去。

代码

#include <cstdio>
#include <iostream>
#define rep(i,s,t) for(int i=s;i<=t;i++)
#define mid (l+r)>>1
#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
using namespace std;
const int MAXN = 3e5 + 233;
int n, M, k, T, tr[MAXN << 2], cnt[MAXN]; //tr记录区间最大值,cnt记录每个箱子的物品个数
void update(int rt)
{
  tr[rt] = max(tr[rt << 1], tr[rt << 1 | 1]);
}
void build(int l, int r, int rt)
{
  if (l == r)
  {
    tr[rt] = M;
    return;
  }
  int m = mid;
  build(lson);
  build(rson);
  update(rt);
}
int modify(int l, int r, int rt, int ai)
{
  if (l == r)
  {
    if (tr[rt] >= ai)
    {
      tr[rt] -= ai;
      if (++cnt[l] == k) //放k个之后直接把剩余容量变成-1
        tr[rt] = -1;
      return l;
    }
    return -1;
  }
  int m = mid, ans;
  if (tr[rt << 1] >= ai) //优先往左,然后往右
    ans = modify(lson, ai);
  else if (tr[rt << 1 | 1] >= ai)
    ans = modify(rson, ai);
  else
    return -1;
  update(rt);
  return ans;
}
int main()
{
  //freopen("in.txt", "r", stdin);
  scanf("%d", &T);
  while (T--)
  {
    scanf("%d%d%d", &n, &M, &k);
    rep (i, 1, n)
      cnt[i] = 0;
    build(root);
    int ai;
    rep (i, 1, n)
    {
      scanf("%d", &ai);
      printf("%d\n", modify(root, ai));
    }
  }
  return 0;
}

B&C 区间不同颜色数

[SDOI2009]HH的项链

题意

给出一个颜色序列,每次询问区间的不同颜色数,数据范围都 1e6。

思路

离线处理,按右端点排序。有两个位置颜色相同时,记录靠右的地方。

  • 从左到右扫一遍颜色数组。
    • 当前位置的颜色没出现过:树状数组这个位置 + 1
    • 当前位置的颜色出现过:上一个位置 -1, 这个位置 + 1
  • 若当前位置恰好是某一个查询的右端点,统计答案 。(也就是为什么优先靠右记录)

核心代码:

sort(q + 1, q + m + 1, cmp1); //右端点排序
int j = 1;
rep (i, 1, n)
{
    if (pos[a[i]])
    modify(pos[a[i]], -1);    //出现过,现在不再统计原来的
    modify(i, 1); //去统计新的
    pos[a[i]] = i;
    while (q[j].r == i) //记得用while
    {
        q[j].ans = query(q[j].r) - query(q[j].l - 1);
        j++;
    }
    if (j == m + 1)
        break;
}
sort(q + 1, q + m + 1, cmp2); //按id排序
rep (i, 1, m)
    printf("%d\n", q[i].ans);

[HEOI2012]采花

题意

跟上题一样,要求所查询区间内具有这个颜色的花至少有两朵。

思路

开两个数组 分别表示颜色 在上次和上上次出现的时间。

要扩展区间右端点时,先看当前颜色有没有出现过。

出现过的话这样子:add(lst[col[i]], 1);

还有别忘了: add(llst[col[i]], -1); //如果llst不是0的话

也就是只有出现两次之后才会把前一次出现的位置对应BIT加一,大概不难想的样子qwq

代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#define rep(i,s,t) for(int i=s;i<=t;i++)
using namespace std;
const int MAXN = 2e6 + 233;
int n, c, m;
int col[MAXN], tr[MAXN], lst[MAXN], llst[MAXN];
struct query
{
  int l, r, id, ans;
} q[MAXN];
bool cmp(query xx, query yy)
{
  return (xx.r == yy.r) ? xx.l < yy.l : xx.r < yy.r;
}
bool cmp2(query xx, query yy)
{
  return xx.id < yy.id;
}
int lb(int x)
{
  return (x) & (-x);
}
void add(int p, int v)
{
  for (; p <= n; p += lb(p))
    tr[p] += v;
}
int getsum(int p)
{
  int ans = 0;
  for (; p; p -= lb(p))
    ans += tr[p];
  return ans;
}
int main()
{
  //freopen("in.txt", "r", stdin);
  scanf("%d%d%d", &n, &c, &m); //n点c颜色(这个值在这个做法里好像没用qwq)m询问
  rep (i, 1, n)
    scanf("%d", &col[i]);
  rep (i, 1, m)
  {
    scanf("%d%d", &q[i].l, &q[i].r);
    q[i].id = i;
  }
  sort(q + 1, q + m + 1, cmp);
  int nowr = 1;
  rep (i, 1, m)
  {
    while (nowr <= q[i].r)
    {
      if (llst[col[nowr]])
        add(llst[col[nowr]], -1);
      if (lst[col[nowr]])
      {
        add(lst[col[nowr]], 1);
        llst[col[nowr]] = lst[col[nowr]];
        lst[col[nowr]] = nowr;
      }
      else
        lst[col[nowr]] = nowr;
      nowr++;
    }
    q[i].ans = getsum(q[i].r) - getsum(q[i].l - 1);
  }
  sort(q + 1, q + m + 1, cmp2);
  rep (i, 1, m)
    printf("%d\n", q[i].ans);
  return 0;
}

F 综合应用

题意

给出一个长 的数组,元素大小 次询问每次给出区间 ,问将区间内元素离散化之后有多少个元素与原来不同。

思路

权值线段树。

区间MEX:区间最小的未出现正整数。

大于MEX的数都会发生改变。

表示值为 的数最后一次在数组中出现的下标,线段树维护区间 最小值。

离线对查询区间右端点排序,对每个询问区间都可以通过区间 值查询出MEX。

问题进一步简化:求区间中小于MEX的数有多少个。

表示值为 的数字有多少个

代码

排序尽量用cmp别operator(operator莫名RE改了好久QAQ)

#include <cstdio>
#include <iostream>
#include <algorithm>
#define rep(i,s,t) for(int i=s;i<=t;i++)
#define root 1,n,1
#define mid (l+r)>>1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
using namespace std;
const int MAXN = 3e5 + 233;
int n, M, a[MAXN], ans[MAXN], q2tot;
struct query1 // q1记录原题询问,对右端点排序
{
  int id, l, r;
} q1[MAXN];
struct query2 // q2记录需要查询出现次数的数字
{
  int id, pos, num, type; //pos位置,num数字,左右端点type分别取1/-1,配合id直接统计到ans里
} q2[MAXN << 1];
bool cmpq1(query1 x, query1 y)
{
  return x.r < y.r;
}
bool cmpq2(query2 x, query2 y)
{
  return x.pos < y.pos;
}
struct Seg
{
  int t[MAXN << 2], mp[MAXN]; //权值线段树,维护lastpos最小值,mp用于快速modify
  void update(int rt)
  {
    t[rt] = min(t[rt << 1], t[rt << 1 | 1]);
  }
  void build(int l, int r, int rt)
  {
    if (l == r)
    {
      t[rt] = 0;
      mp[l] = rt;
      return;
    }
    int m = mid;
    build(lson);
    build(rson);
    update(rt);
  }
  void modify(int rt, int v)
  {
    rt = mp[rt];
    t[rt] = v;
    while (rt >>= 1)
      update(rt);
  }
  int query(int l, int r, int rt, int nl) //查询nl以后的mex
  {
    if (l == r)
    {
      return l;
    }
    int m = mid;
    if (t[rt << 1] < nl) //左边有数字最后一次出现在nl之前,优先往左查
      return query(lson, nl);
    else
      return query(rson, nl);
  }
} ST;
struct Bit
{
  int t[MAXN << 1]; //统计数字出现次数的BIT
  int lb(int x)
  {
    return x & (-x);
  }
  void modify(int p, int v)
  {
    for (; p <= n; p += lb(p))
      t[p] += v;
  }
  int query(int p)
  {
    int tmp = 0;
    for (; p; p -= lb(p))
      tmp += t[p];
    return tmp;
  }
} BIT;
int main()
{
  //freopen("in.txt", "r", stdin);
  scanf("%d", &n);
  rep (i, 1, n)
  {
    scanf("%d", &a[i]);
    if (a[i] > n)
      a[i] = n + 1;
  }
  scanf("%d", &M);
  rep (i, 1, M)
  {
    scanf("%d%d", &q1[i].l, &q1[i].r);
    q1[i].id = i;
    //一开始认为区间所有数字的值都会改变,后面去查有多少不变(有多少数字小于等于mex)
    ans[i] = q1[i].r - q1[i].l + 1;
  }
  sort(q1 + 1, q1 + M + 1, cmpq1);
  int now = 1;
  ST.build(root);
  rep (i, 1, n)
  {
    ST.modify(a[i], i); //更新当前数字最后出现位置
    while (now <= M && q1[now].r == i)
    {
      q2[++q2tot].id = q1[now].id;
      q2[q2tot].num = ST.query(root, q1[now].l); //查询当前mex
      //前缀和,出现次数 = [1, r] - [1, l - 1],ans是要减去这个数,右端点记-1
      q2[q2tot].pos = q1[now].r;
      q2[q2tot].type = -1;
      if (q1[now].l != 1)
      {
        q2[++q2tot].id = q1[now].id;
        q2[q2tot].num = q2[q2tot - 1].num;
        q2[q2tot].pos = q1[now].l - 1;
        q2[q2tot].type = 1;
      }
      now++;
    }
  }
  //按pos排序,边添加数字边查询,就能避免用可持久化数据结构了emm
  sort(q2 + 1, q2 + q2tot + 1, cmpq2);
  now = 1;
  rep (i, 1, n)
  {
    BIT.modify(a[i], 1);
    while (now <= q2tot && q2[now].pos == i)
    {
      ans[q2[now].id] += BIT.query(q2[now].num) * q2[now].type;
      now++;
    }
  }
  rep (i, 1, M)
    printf("%d\n", ans[i]);
  return 0;
}
全部评论

相关推荐

点赞 评论 收藏
分享
10-10 01:10
已编辑
深圳大学 测试开发
面了100年面试不知...:六月到九月,四个项目一个实习,是魔丸吗
投了多少份简历才上岸
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务