操作树学习笔记

前言

这个数据结构(或许不是数据结构?)比较冷门,网上也没找到什么博客。我尽量讲得详细。

做一道CF的题目的时候就遇到了"操作树"。

这东西蛮简单的,但是有些时候却挺有用的。

一般可以用操作树的题目:

  • 允许离线(这是最重要的)

  • 需要查询历史版本

  • 当前操作受到之前操作的影响。

  • 需要维护的状态不是那么复杂

操作树的定义:

对于操作树上的节点 就存储操作 完成之后的状态。

操作树该怎么建?

以这道题目为例:

CF707D Persistent Bookcase

题意搬运:

维护一个二维零一矩阵(),支持四种操作(不超过次):

  • 将第 反转
  • 回到第 次操作后的状态可以为

每次操作后输出全局一共有多少个

解法

这道题目实际上最难办的地方是操作

但是,这个问题没有强制在线

对于一个操作 我们发现倘若它不是第四种操作,它的状态一定是在上一次操作的基础上进行修改的。

那么对于非操作 的所有操作,根据操作树的定义,想到建一条边 ,表示操作 的状态需要延用到操作 ,也就是操作仍然需要它的状态。

实际上对于操作 ,我们连一条边 ,不需要连 ,这样即可。因为操作4只需要知道第 次操作后的状态,于是我们用操作 的状态来转移。

建树建完后,我们采用 的方式来处理每一次操作后的答案。在本题中,我们开一个二维的 来维护状态,每次的答案是对应状态 中的二维 的数量,这个是很容易统计的。

这样弄完之后有什么好处?

这样的话可以让我们减少空间的浪费,而且可以支持回溯到某一个版本的操作。

我们来模拟一下建树的过程:

先是状态 ,矩阵全部都是 ,也就是没有修改。

然后有了操作1,它沿用的操作0的状态(即初始状态)

接着有了操作二,它需要沿用操作的状态

再是操作三,它沿用操作2的状态:

后来是操作4,注意,它属于操作类型4,它需要访问第1次修改后的状态,于是将操作4连到操作1下面:

然后又来了一个操作5,这个操作应该沿用操作4的状态,于是连到操作4的下面。

最后是操作6,注意,它属于操作类型4,它需要沿用操作3的状态,于是将操作6连到操作3的下面

然后就建树完成了!(这是最核心的一部分)

怎么获得答案?对这棵树进行,对于操作1,2,3就暴力修改,用即可,然后对于操作4我们不需要修改,直接沿用前面的状态即可。

遍历完后记得回溯,具体看代码。

Code

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005,MAXM = 1005;
bitset <MAXM> All,P[MAXM];//All全部为1,便于对一整行取反

int n , m , Q;
int start[MAXN],cnt = 0;
int Ans[MAXN];

struct Node {
    int op,x,y,id;
} T[MAXN];

struct Edge {
    int next,to;
} edge[MAXN];

void add(int from,int to)
{
    cnt ++;
    edge[cnt].to = to;
    edge[cnt].next = start[from];
    start[from] = cnt;
    return ;
}

void DFS(int x,int from,int Now)//x表示当前节点,from表示当前节点的父亲节点,Now表示的是当前状态的答案
{
    bool q = 0;
    if(T[x].op == 1)
    {
        q = P[T[x].x][T[x].y];
        if(P[T[x].x][T[x].y] == 0)Now ++;//更新答案
        P[T[x].x][T[x].y] = 1;//直接修改
    }
    if(T[x].op == 2)
    {
        q = P[T[x].x][T[x].y];
        if(P[T[x].x][T[x].y] == 1)Now --;//更新答案
        P[T[x].x][T[x].y] = 0;
    }
    if(T[x].op == 3)
    {
        int A = P[T[x].x].count();//原来有多少个1
        P[T[x].x] ^= All;//暴力修改
        Now += P[T[x].x].count() - A;//统计现在的1的个数减去原来1的个数即是答案
    }
      //对于操作4不需要做出修改,直接沿用上次操作的答案即可
    Ans[T[x].id] = Now;//将答案塞进答案序列
    for(int i = start[x] ; i ; i = edge[i].next)
    {
        int to = edge[i].to;
        if(to != from)DFS(to,x,Now);//遍历
    }
    if(T[x].op <= 2)P[T[x].x][T[x].y] = q;//状态回溯
    if(T[x].op == 3)P[T[x].x] ^= All;//状态回溯
    return ;
}

inline int read()
{
    int x = 0, flag = 1;
    char ch = getchar();
    for( ; ch > '9' || ch < '0' ; ch = getchar())if(ch == '-')flag = -1;
    for( ; ch >= '0' && ch <= '9' ; ch = getchar())x = (x << 3) + (x << 1) + ch - '0';
    return x * flag;
}

int main()
{
    n = read() , m = read() ,Q = read();
    for(int i = 1 ; i <= m ; i ++)All[i] = 1;
    for(int i = 1 ; i <= n ; i ++)P[i].reset();//全部置0
    T[0].x = T[0].y = 0;
    for(int i = 1 ; i <= Q ; i ++)
    {
        T[i].op = read();T[i].id = i;//离线操作的常用套路,记录它的第几个询问
        if(T[i].op <= 2)T[i].x = read(),T[i].y = read();
        else T[i].x = read();
        if(T[i].op <= 3)add(i - 1 , i);//如果不属于操作4就连边i-1 to i
        else add(T[i].x,i);//如果属于操作4就连边K to i
    }
    DFS(0,0,0);//注意要从0开始遍历,因为K可以等于0!
    for(int i = 1 ; i <= Q ; i ++)cout << Ans[i] << endl;//输出答案即可
    return 0;
}
全部评论

相关推荐

牛客722552937号:新锐之星有点坑爹,特别是对男的
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务