P1383 高级打字机

原题链接  https://www.luogu.org/problemnew/show/P1383

作为考试的T1,我的内心也是很绝望的,最难的题是T1?

首先前50分还是很好拿的,考场上许多人都水到了这50分,做法就是简单的数组模拟:

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
int n,tail,x;            //tail是有几步添加操作 
char p,m,wz[100001];
int main()
{
    //freopen("type.in","r",stdin);
    //freopen("type.out","w",stdout);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        cin>>p;
        if(p=='T') cin>>m,wz[++tail]=m;       //添加
        else scanf("%d",&x);
        if(p=='U') tail-x>=0?tail-=x:tail=0;  //删除后x个元素,注意栈内元素不足x的情况       
        if(p=='Q') printf("%c\n",wz[x]);            
    }
    return 0;
}

考虑100分的做法:

我们用栈将每一步操作都存下来,下面以样例为例:

接下来的操作是询问操作,我们直接找到第x个元素输出就可以了:

下面一步是撤销操作,撤销了两步,所以我们直接从当前第3步操作往前找两步到第1步操作来作为我们的第4步操作:

继续插入'c':

然后查询第二个元素:

这样我们就实现了撤销功能,但是每一步操作都要拷贝前一种情况,占内存太大,显然MLE。

想想为什么空间内存会炸?你每次操作都将原数列拷贝一份再进行操作,这样一来其实有很多空间是被考来考去而不被利用的,造成了极大的空间浪费!

所以我们可以考虑用链式前向星来维护栈,它的思想是每插入一个元素我们就开辟一个新的结点指向上一个,这样我们每一步的操作不用再将之前的元素拷贝过来了,节省了很大的空间

首先我们用top表示一个空栈,插入一个元素 'a':

插入一个元素 'b':

插入一个元素 'c':

询问第二个元素,在链表里从前往后找到第二个元素:

撤销两步操作,从最后一个元素往前找两步,创建新节点记录:

插入一个元素 'c':

询问第二个元素,从前往后找到第二个元素:

考虑到插入和撤销操作都是O(1)完成,但是查询的时候我们要先在所以的链中找出当前那一条链,如果撤销操作多了时间也是会爆炸的!还要考虑一个更为稳定的算法。

分块大法好!!!

继承上面的思路,我们改进用分块。

浅谈分块

所谓分块,顾名思义就是分成好几块。我们在用链式前向星存的时候,不再是每个结点只存一个元素了,我们每个结点存一个数组!

这样有什么好处呢?我们可以O(N / B)找到某个元素在哪!(N表示这个元素在栈里是第N个元素,每个块的大小是B)

每个块的数组大小是由我们设定的,所以当我们访问的时候可以通过 N / B 来求出前面有多少个块,然后剩下的就在一个块里了,利用数组查询的原理直接O(1)查询;

 

所以我们在用链表存的时候稍加一些修改就好啦:

首先我们先开一个结构体:

struct node
{
    char *s;
    int now, pc;                               //now是当前块里有多少个元素,pc是当前块前面有多少个块 
    node *pre;                                 //当前块的前一个是第几块 
    node()                                     //构造函数 
    {
        now = pc = 0;
        s = new char[B];                       //存每个块数组的元素 
        pre = NULL;
    }
    void copy(node *src)                       //构造函数 
    {
        now = src->now;
        pc = src->pc;
        pre = src->pre;
        for (int i = 0; i < now; i++)
        {
            s[i] = src->s[i];
        }
    }
} * head[N]; 

我们在插入新元素的时候,考虑有如下两种情况:

1.当前块没有满,那么我们就直接在后面开一个空位置插入就可以了;

2.若当前块满了,那么我们就新开一个块插入,注意更新 pc,* pre 的值;

void append(char x)                            //添加一个新元素 
{
    if (head[lst]->now == B)                   //如果当前的块满了,开辟一个新块 
    {
        head[++lst] = new node;                //开一个结构体类型的空间 
        head[lst]->pre = head[lst - 1];        //lst的前一个块是lst-1 
        head[lst]->pc = head[lst - 1]->pc + 1; //lst前面的块数比lst-1前面的块数多1 
        head[lst]->s[head[lst]->now++] = x;    //添加新元素 
    }
    else                                       //如果块没满 
    {
        head[++lst] = new node;
        head[lst]->copy(head[lst - 1]);        //拷贝一个和上个块一毛一样的块 
        head[lst]->s[head[lst]->now++] = x;    //添加新元素 
    }
}

询问第x个元素的时候,由于栈是后进先出的结构,而我们要从前往后找,这就有点挠人啊;

这时候分块的作用就出来了:我们可以先求出栈内的元素个数:完整块数(数组中没有空位置) * 块的数组大小 + 当前这个块中的元素个数

    int siz = head[lst]->now + head[lst]->pc * B; //看看栈里有几个元素,now+块数*每块有几个 

求出有几个元素之后,我们要从前往后找x个,不就相当于从后往前找 siz - x + 1 个嘛?正好符合栈的特点。

设 y = siz - x + 1,我们先判断这个 y 是否就在当前块范围内(y <=now),如果在的话就不用再往下找了;

如果不在的话,我们先让 y 减去 now,然后我们往前找,如果过程中 y 大于 B,那么就跳过当前块到上一个块,直到 y <=B,说明该元素就在当前块里了,那么我们直接输出就好啦;

例如:

假设我们要求第3个元素,先求出所有的元素个数:2*6+2=14,那么就相当于从后往前找第 14-3+1 = 12 个元素,我们发现 12 > now,说明不在当前最后的那个块里,那么先 12 - 2 =10,意思是找完了最后一块后我们还要往前找 10 个元素;由于 10 > B,那么我们直接跳过第二块来到第一块,我们还需要找 10 - 6 = 4 个元素;发现 4 < 6,说明就在当前块里,利用数组直接输出 a [ 6 - 4 ] = a [ 2 ] = c,注意这里我们还是要再转换成从前往后找的。

 

撤销操作是最简单的,我们先设 lat 为当前这是第 lat 步操作,如果我们撤销了 x 步操作,那么直接让第 lst + 1 步操作的指针指向第 lst - x 步操作就OK了;

 

完整指针版本的AC代码: 

#include <cmath>
#include <cstdio>
#include <cstring>
template <class T>
inline void read(T &num)
{
    bool flag = 0;
    num = 0;
    char c = getchar();
    while ((c < '0' || c > '9') && c != '-')
        c = getchar();
    if (c == '-')
    {
        flag = 1;
        c = getchar();
    }
    num = c - '0';
    c = getchar();
    while (c >= '0' && c <= '9')
        num = (num << 3) + (num << 1) + c - '0', c = getchar();
    if (flag)
        num *= -1;
}
inline void read(char &c)
{
    do
    {
        c = getchar();
    } while (c == ' ' || c == '\n' || c == '\r');
}
template <class T>
inline void output(T num)
{
    if (num < 0)
    {
        putchar('-');
        num = -num;
    }
    if (num >= 10)
        output(num / 10);
    putchar(num % 10 + '0');
}
template <class T>
inline void outln(T num)
{
    output(num);
    putchar('\n');
}
template <class T>
inline void outps(T num)
{
    output(num);
    putchar(' ');
}
template <class T>
inline T max(T a, T b)
{
    return a < b ? b : a;
}
const int N = 100000;
int n, B;
struct node
{
    char *s;
    int now, pc;                               //now是当前块里有多少个元素,pc是当前块前面有多少个块 
    node *pre;                                 //当前块的前一个是第几块 
    node()                                     //构造函数 
    {
        now = pc = 0;
        s = new char[B];                       //存每个块数组的元素 
        pre = NULL;
    }
    void copy(node *src)                       //构造函数 
    {
        now = src->now;
        pc = src->pc;
        pre = src->pre;
        for (int i = 0; i < now; i++)
        {
            s[i] = src->s[i];
        }
    }
} * head[N];                                   //一个指针数组 
int lst;                                       //当前这是第几步操作 
void append(char x)                            //添加一个新元素 
{
    if (head[lst]->now == B)                   //如果当前的块满了,开辟一个新块 
    {
        head[++lst] = new node;                //开一个结构体类型的空间 
        head[lst]->pre = head[lst - 1];        //lst的前一个块是lst-1 
        head[lst]->pc = head[lst - 1]->pc + 1; //lst前面的块数比lst-1前面的块数多1 
        head[lst]->s[head[lst]->now++] = x;    //添加新元素 
    }
    else                                       //如果块没满 
    {
        head[++lst] = new node;
        head[lst]->copy(head[lst - 1]);        //拷贝一个和上个块一毛一样的块 
        head[lst]->s[head[lst]->now++] = x;    //添加新元素 
    }
}
char query(int x)                              //查询操作 
{
    int siz = head[lst]->now + head[lst]->pc * B; //看看栈里有几个元素,now+块数*每块有几个 
    x = siz - x + 1;                           //从前往后找x个,相当于从后往前找siz-x+1个 
    if (x <= head[lst]->now)                   //如果在当前块里,直接输出就好了 
    {
        return head[lst]->s[head[lst]->now - x];
    }
    x -= head[lst]->now;                       //减去当前块元素的个数,继续往前找 
    node *now = head[lst]->pre;                //移到当前块的前一个块  
    while (x > B)                              //如果一直大于每个块的最大块数,也就是说可以跳块,那就往前跳 
    {
        now = now->pre;                        //跳到前一个 
        x -= B;                           
    }
    return now->s[B - x];                      //跳完之后一定在当前块里,直接输出,因为我们是从后往前找,所以正确答案是B-x 
}
int main()
{
    read(n);
    B = sqrt(n);
    head[0] = new node;
    for (int i = 1; i <= n; i++)
    {
        char op;
        read(op);
        if (op == 'T')                         //添加一个元素 
        {
            char x;
            read(x);
            append(x);
        }
        if (op == 'U')                         //撤销操作 
        {
            int x;
            read(x);
            head[lst + 1] = head[lst - x];     //撤销x步,直接将第lst+1步的指针指向lst往前x步就好了 
            lst += 1;                                        
        }
        if (op == 'Q')                         //查询操作 
        {
            int x;
            read(x);
            putchar(query(x));                 //查询栈里的第x个元素 
            putchar('\n');
        }
    }
}

 

posted @ 2019-07-12 20:45  暗い之殇  阅读(251)  评论(0编辑  收藏  举报