2020安徽省大学生程序设计大赛题解——G 序列游戏

2020安徽省大学生程序设计大赛题解——G 序列游戏

G 序列游戏

有一个序列w,初始为空。再给出一个长度为m单调递增的序列a。你需要对序列w作如下n次操作:
(1)操作0,在序列尾部添加数字0。
(2)操作1,在序列尾部添加数字1.
(3)操作-1,删除序列w中,所有位于位置ai的数(1<=i<=m)。比如 a={1,3,5},就将w中第1,3,5个数删除。若ai>w 的当前长度,则该操作停止。
输出n次操作后的序列w。

题目

在这里插入图片描述

标签

树状数组、二分查找

分析

本题最大的两个特征为数据范围大,对数据的操作次数多。基于这两个特点,我们采用两个对应的方法,即二分查找(以便在数据范围大时寻找数据)和树状数组(以便删除数据后,数组可以快速调整)。

二分无需赘述,我们重点介绍一下树状数组。

树状数组,顾名思义,就是用数组来模拟树形结构。那么衍生出一个问题,为什么不直接建树?答案是没必要,因为树状数组能处理的问题就没必要建树。我们仅需两个数组便能存储和快速处理树状数组的各项数据。

这里采用树状数组,是因为其修改和查询的复杂度都是 O ( l o g N ) O(logN) O(logN),而且相比线段树系数要少很多,比传统数组要快,而且容易写。

下面是关于树状数组的基础概念。

二叉树大家一定都知道,如下图:
图G-1
图 G − 1      经 典 二 叉 树 的 图 示 图G-1 \ \ \ \ 经典二叉树的图示 G1    

如果每个父亲都存的是两个儿子的值,这样的树形结构,叫做线段树。这里不用线段树是因为线段树虽然搜索速度快,但调整速度不如树状数组,另外,线段树解决此题显得有点大材小用。

真正的树状数组和上图类似,但省去了一些节点,以便用数组建树。

图G-2
图 G − 2      树 状 数 组 的 一 个 图 示 ( 图 源 网 络 ) 图G-2 \ \ \ \ 树状数组的一个图示(图源网络) G2    

黑色数组代表原来的数组(下面用 A [ i ] A[i] A[i]代替),红色结构代表我们的树状数组(下面用 C [ i ] C[i] C[i]代替),发现没有,每个位置只有一个方框,令每个位置存的就是子节点的值的和,则有:

  • C [ 1 ] = A [ 1 ] ; C[1] = A[1]; C[1]=A[1];
  • C [ 2 ] = A [ 1 ] + A [ 2 ] ; C[2] = A[1] + A[2]; C[2]=A[1]+A[2];
  • C [ 3 ] = A [ 3 ] ; C[3] = A[3]; C[3]=A[3];
  • C [ 4 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] ; C[4] = A[1] + A[2] + A[3] + A[4]; C[4]=A[1]+A[2]+A[3]+A[4];
  • C [ 5 ] = A [ 5 ] ; C[5] = A[5]; C[5]=A[5];
  • C [ 6 ] = A [ 5 ] + A [ 6 ] ; C[6] = A[5] + A[6]; C[6]=A[5]+A[6];
  • C [ 7 ] = A [ 7 ] ; C[7] = A[7]; C[7]=A[7];
  • C [ 8 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] + A [ 5 ] + A [ 6 ] + A [ 7 ] + A [ 8 ] ; C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]; C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

可以发现,这颗树是有规律的:

C [ i ] = A [ i − 2 k + 1 ] + A [ i − 2 k + 2 ] + . . . + A [ i ] C[i] = A[i - 2^k+1] + A[i - 2^k+2] + ... + A[i] C[i]=A[i2k+1]+A[i2k+2]+...+A[i],其中 k k k i i i的二进制中从最低位到高位连续零的长度。

例如 i = 8 ( 1000 ) i = 8(1000) i=8(1000)时候, k = 3 k = 3 k=3,可自行验证。

这个怎么实现求和呢,比如我们要找前 7 7 7项和,那么应该是 S U M = C [ 7 ] + C [ 6 ] + C [ 4 ] SUM = C[7] + C[6] + C[4] SUM=C[7]+C[6]+C[4]

而根据上面的式子,容易的出 S U M i = C [ i ] + C [ i − 2 k 1 ] + C [ ( i − 2 k 1 ) − 2 k 2 ] + . . . SUM_i = C[i] + C[i-2^{k1}] + C[(i - 2^{k1}) - 2^{k2}] + ... SUMi=C[i]+C[i2k1]+C[(i2k1)2k2]+...

其实树状数组就是一个二进制上面的应用。

现在新的问题来了, 2 k 2^k 2k该怎么求呢,不难得出 2 k = i & ( i ( i − 1 ) ) 2^k = i\&(i^{(i-1)}) 2k=i&(i(i1)),但这个还是不好求出,前辈的智慧就出来了, 2 k = i & ( − i ) 2^k = i\&(-i) 2k=i&(i)。我们引用数论里的证明过程对此进行说明。

这里利用的负数的存储特性,负数是以补码存储的,对于整数运算 x & ( − x ) x\&(-x) x&(x)

  • x x x 0 0 0时,即 0 & 0 0 \& 0 0&0,结果为 0 0 0
  • x x x为奇数时,最后一个比特位为 1 1 1,取反加 1 1 1没有进位,故 x x x − x -x x除最后一位外前面的位正好相反,按位与结果为 0 0 0。结果为 1 1 1
  • x x x为偶数,且为 2 2 2 m m m次方时, x x x的二进制表示中只有一位是 1 1 1(从右往左的第 m + 1 m+1 m+1位),其右边有 m m m 0 0 0,故 x x x取反加 1 1 1后,从右到左第有 m m m 0 0 0,第 m + 1 m+1 m+1位及其左边全是 1 1 1。这样, x & ( − x ) x\& (-x) x&(x) 得到的就是x。
  • x x x为偶数,却不为 2 2 2 m m m次方的形式时,可以写作 x = y ∗ ( 2 k ) x = y * (2^k) x=y(2k)。其中, y y y的最低位为 1 1 1。实际上就是把x用一个奇数左移 k k k位来表示。这时, x x x的二进制表示最右边有 k k k 0 0 0,从右往左第 k + 1 k+1 k+1位为 1 1 1。当对 x x x取反时,最右边的 k k k 0 0 0变成 1 1 1,第 k + 1 k+1 k+1位变为 0 0 0;再加 1 1 1,最右边的 k k k位就又变成了 0 0 0,第 k + 1 k+1 k+1位因为进位的关系变成了 1 1 1。左边的位因为没有进位,正好和 x x x原来对应的位上的值相反。二者按位与,得到:第 k + 1 k+1 k+1位上为 1 1 1,左边右边都为 0 0 0。结果为 2 k 2^k 2k
  • 总结一下: x & ( − x ) x\&(-x) x&(x),当 x x x 0 0 0时结果为 0 0 0 x x x为奇数时,结果为 1 1 1 x x x为偶数时,结果为 x x x 2 2 2的最大次方的因子。

这个有一个专门的称呼,叫做 l o w b i t lowbit lowbit,即取 2 k 2^k 2k

基于上述描述,我们给出树状数组的构造程序(非原创):

int n;
int a[1005],c[1005]; //对应原数组和树状数组

int lowbit(int x){
    return x&(-x);
}

void add(int i,int k){    //在i位置加上k
    while(i <= n){
        c[i] += k;
        i += lowbit(i);
    }
}

int getsum(int i){        //求A[1 - i]的和
    int res = 0;
    while(i > 0){
        res += c[i];
        i -= lowbit(i);
    }
    return res;
}

有了树状数组和二分查找,本题的思路就显得十分清晰。本题有三个操作,其中一二两个操作本质上毫无区别,我们给出对应的树状数组的处理策略:

  • 添加数:添加数进入树状数组,指针后移
  • 删除数:二分找到待删数,删去,树状数组调整

完整的操作详见程序。

参考答案(C++)
#include<bits/stdc++.h>
using namespace std;

namespace fast_IO {
    inline int read() {
        int x = 0, f = 1; char ch = getchar();
        while (!isdigit(ch)) { if (ch == '-') f = -1; ch = getchar(); }
        while (isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
        return x * f;
    }
};
using namespace fast_IO;

const int N = 1e6 + 5;
int c[N], a[N], ans[N], top, as[N];
int n, m, sum;

int lowbit(int x) {
    return x & (-x);
}

void add(int x, int val) {
    while (x <= m) {
        c[x] += val;
        x += lowbit(x);
    }
}

int getsum(int x) {
    int ans = 0;
    while (x) {
        ans += c[x];
        x -= lowbit(x);
    }
    return ans;
}

void solve(int num, int ct) {
    int l = 1, r = m;
    while (l < r) {
        int mid = (l + r) / 2;
        if (getsum(mid) >= num)
            r = mid;
        else
            l = mid + 1;
    }
    as[ct] = l;
}

int t;

int main() {
    cin >> m >> n;
    memset(ans, -1, sizeof(ans));
    for (int i = 1; i <= n; i++) {
        a[i] = read();
    }
    for (int i = 1; i <= m; i++) {
        t = read();
        if (t == 0 || t == 1) {
            ans[++top] = t;
            sum++;
            add(top, 1);
        }
        else {
            int temp = lower_bound(a + 1, a + n + 1, sum) - a;
            sum -= temp > n || a[temp] > sum ? --temp : temp;
            for (int i = 1; i <= temp; i++)
                solve(a[i], i);
            for (int i = 1; i <= temp; i++) {
                add(as[i], -1);
                ans[as[i]] = -1;
            }
        }
    }
    bool flag = false;
    for (int i = 1; i <= m; i++) {
        if (ans[i] == -1) continue;
        cout << ans[i];
        flag = true;
    }
    if (!flag) cout << "Poor stack!";
    return 0;
}
  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值