CCF 202012-5星际旅行(20~100分)

Posted 幕无

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CCF 202012-5星际旅行(20~100分)相关的知识,希望对你有一定的参考价值。

前置知识

线段树:通过懒惰标记,可实现区间处理,和区间询问皆为\\(O(logn)\\)时间复杂度的数据结构,是一种二叉树。因此对于一个节点\\(st\\),其左儿子节点为\\(st*2\\),右节点为\\(st*2+1\\),为方便整洁,代码中使用宏定义\\(ls(st<<1)\\)\\(rs(st<<1|1)\\)

离散化:将无法通过开数组标记的大数据变为小数据的一种手段。

分析

\\(20\\)分攻略

算法:模拟

由于\\(n,m\\leq 1000\\),此时\\(O(nm)\\)的算法是允许的,所以对于每一种操作用循环模拟即可。

\\(40\\)分攻略

算法 :差分+树状数组 \\(OR\\) 线段树+懒惰标记\\(*1\\)

\\(n\\leq 100000,m\\leq 40000\\),且只包含\\(1,4\\)操作。需要使用时间复杂度带\\(log\\)的算法,使用数据结构树状数组+差分或者线段树+懒惰标记即可。

线段树传递懒惰标记时,要乘以区间长度。由于有三维,所以开三个线段树即可,时间复杂度\\(O(mlogn)\\)

struct node {
    int lazS, lazM, sum;
} t[3][maxn << 3];

\\(60\\)分攻略

算法:线段树+懒惰标记\\(*2\\)

\\(n\\leq 100000,m\\leq 40000\\),且只包含\\(1,2,4\\)操作。同样需要使用时间复杂度带\\(log\\)的算法,但是此时有两种操作,树状数组不能应用于如此复杂的情况。

使用线段树时,需要考虑两个懒惰标记相互的影响。设加法懒惰标记为\\(lazS\\),乘法懒惰标记为\\(lazM\\)。由于标记的实际含义是保留左节点和右节点的未操作信息,所以懒惰标记下传时,由乘法分配律,\\(lazS\\)要乘以\\(lazM\\),这样可以保证在\\(lazS\\)下传的时候,不会丢失\\(lazM\\)的信息。

考虑标记下传时是先进行乘法操作还是先进行加法操作。

  • 若是先进行加法操作,会出现将乘法操作后的新的加法标记同时乘上乘法标记。
  • 若是先进行乘法操作,将乘法操作先作用于原有的和上,然后再将加法操作作用上去即可。
#define ls st<<1
#define rs st<<1|1
void push_down(int st);
t[i][ls].sum = ((t[i][ls].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[ls]) % mod;//先进行乘法操作,再进行加法操作
t[i][rs].sum = ((t[i][rs].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[rs]) % mod;//先进行乘法操作,再进行加法操作
t[i][ls].lazS = ((t[i][ls].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
t[i][rs].lazS = ((t[i][rs].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
t[i][ls].lazM = t[i][ls].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
t[i][rs].lazM = t[i][rs].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
t[i][st].lazM = 1;//清除懒惰标记
t[i][st].lazS = 0;//清除懒惰标记

时间复杂度\\(O(mlogn)\\),此时题目为洛谷\\(P3372\\)

\\(80\\)分攻略

算法:线段树+懒惰标记\\(*3\\)

\\(n\\leq 100000\\),引入转换操作,同样需要使用时间复杂度带\\(log\\)的算法。

首先先考虑没有\\(1,2\\)操作时应该如何做,注意到每次操作由原来的\\(x,y,z\\)变为\\(y,z,x\\)相当于原来的三维元素向左平移一维,那么如果向左平移三次,就相当于没有平移,所以转换的转移标记\\(lazS\\)进行\\(\\%3\\)操作即可。

再考虑有\\(1,2\\)操作时应该如何做,注意到加法乘法其实和转换操作没有关系,所以只需要考虑数字运算(加法和乘法)和转换运算这两种运算是如何相互影响的。

再次考虑懒惰标记的实际含义,是将左节点和右节点尚未处理的信息保存起来,那么转换时也需将保存的信息同时转换。

if (lazC[rt] == 1) {//向左平移一维
    node x = t[0][st], y = t[1][st], z = t[2][st];
    t[0][st] = y;
    t[1][st] = z;
    t[2][st] = x;
} else if (lazC[rt] == 2) {//向左平移两维
    node x = t[0][st], y = t[1][st], z = t[2][st];
    t[0][st] = z;
    t[1][st] = x;
    t[2][st] = y;
}

再考虑是先进行转换运算还是先进行数字运算,当操作子节点的时候,父节点已经进行了转换,所以为了让父节点的标记操作能够对应上子节点的标记,所以要先进行转换,再进行数字运算。

\\(100\\)分攻略

算法:线段树+懒惰标记\\(*3\\)+区间离散化

\\(n≤1,000,000,000\\),原有线段树空间复杂度为\\(O(n)\\),但现在这个数据范围不支持这个复杂度,考虑离散化。

注意到\\(m\\leq 40000\\),那么操作的区间个数最多为\\(40000\\)个,端点最多为\\(80000\\)个,那么离散化后进行操作空间复杂度为\\(O(m)\\),注意到线段树开点应开总端点的\\(4\\)倍,所以总共需要开\\(8m\\)个点,即为\\(m<<3\\)

使用区间离散化的技巧,将原有的\\([L,R]\\)区间变为\\([L,R+1)\\),这样满足区间可加性,举个反例体会下这个操作的必要性。

提供三个操作\\([1,4],[1,1],[4,4]\\),直接离散化后,区间为\\([1,2],[1,1],[2,2]\\),后两个操作合并等于第一个操作,而原有操作中的端点\\(2\\)\\(3\\)丢失了。而转换为左闭右开区间后,区间为\\([1,5),[1,2),[4,5)\\),离散化后为\\([1,4),[1,2),[3,4)\\),此时信息并未丢失,保存每一个区间的长度\\(len[maxn << 3]\\)即可。

原线段树的叶子节点理解为每一个端点,离散化区间后的叶子节点可以理解为以第\\(i\\)个端点为开头的左闭右开区间。比如说上述转换区间后为\\([1,5),[1,2),[4,5)\\),那么叶子节点会变为\\([1,2),[2,4),[4,5)\\),一共出现过\\(4\\)个不同的区间端点,显然最右边的端点不需要保存信息,所以一共有三个叶子节点,第\\(1\\)个叶子节点表示以\\(1\\)开头的左闭右开区间,第\\(2\\)个叶子节点表示以\\(2\\)为开头的左闭右开区间,第\\(3\\)个叶子节点表示以\\(4\\)为开头的左闭右开区间(代码中最后一个端点也有一个节点,长度为0)。

考虑操作,考虑第一个操作\\([1,5)\\)(注意离散化后区间为\\([1,4)\\)),他需要的是\\(5\\)之前的端点的操作,即要操作\\(1,2,4\\)(离散化后为\\(1,2,3\\))开头的区间,那么由于线段树保留的是以第\\(i\\)个端点为开头的左闭右开区间,所以操作时要将离散化后的右端点\\(4\\)减一再进行操作。

#include <bits/stdc++.h>

#define start ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define ll long long
#define int ll
#define ls st<<1//左子树,st*2
#define rs st<<1|1//右子树,st*2+1
using namespace std;
const int maxn = (ll) 4e5 + 5;
const int mod = 1e9 + 7;
int T = 1;
vector<int> v;
struct node {
    int lazS, lazM, sum;
} t[3][maxn << 3];
int lazC[maxn << 3];
int len[maxn << 3];
int q[maxn][6];

void build(int st, int l, int r) {
    for (int i = 0; i <= 2; ++i)t[i][st].lazM = 1;
    /*
     * 最右区间长度为0
     * 由于保留左闭右开区间,所以长度为v[l + 1] - v[l]
     */
    if (l == r) {
        if (l == v.size() - 1)
            len[st] = 0;
        else
            len[st] = v[l + 1] - v[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    len[st] = len[ls] + len[rs];
}

void change(int rt, int st) {
    if (lazC[rt] == 1) {//向左平移一维
        node x = t[0][st], y = t[1][st], z = t[2][st];
        t[0][st] = y;
        t[1][st] = z;
        t[2][st] = x;
    } else if (lazC[rt] == 2) {//向左平移两维
        node x = t[0][st], y = t[1][st], z = t[2][st];
        t[0][st] = z;
        t[1][st] = x;
        t[2][st] = y;
    }
}

void push_down(int st) {
    if (lazC[st]) {//先进行转换操作
        change(st, ls);
        change(st, rs);
        lazC[ls] = (lazC[ls] + lazC[st]) % 3;
        lazC[rs] = (lazC[rs] + lazC[st]) % 3;
        lazC[st] = 0;
    }
    for (int i = 0; i <= 2; ++i) {
        t[i][ls].sum = ((t[i][ls].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[ls]) % mod;//先进行乘法操作,再进行加法操作
        t[i][rs].sum = ((t[i][rs].sum * t[i][st].lazM) % mod + t[i][st].lazS * len[rs]) % mod;//先进行乘法操作,再进行加法操作
        t[i][ls].lazS = ((t[i][ls].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
        t[i][rs].lazS = ((t[i][rs].lazS * t[i][st].lazM) % mod + t[i][st].lazS) % mod;//注意加法标记要先做乘法,再做加法
        t[i][ls].lazM = t[i][ls].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
        t[i][rs].lazM = t[i][rs].lazM * t[i][st].lazM % mod;//乘法标记直接操作即可
        t[i][st].lazM = 1;//清除懒惰标记
        t[i][st].lazS = 0;//清除懒惰标记
    }
}

void add(int st, int l, int r, int L, int R, int a[]) {
    if (L <= l && r <= R) {
        for (int i = 0; i <= 2; ++i) {
            t[i][st].lazS = (t[i][st].lazS + a[i]) % mod;
            t[i][st].sum = (t[i][st].sum + a[i] * len[st]) % mod;//注意lazS保留的仅仅是加法,以便对于不同区间根据长度判定加和
        }
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        add(ls, l, mid, L, R, a);
    if (R > mid)
        add(rs, mid + 1, r, L, R, a);
    for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
}

void mul(int st, int l, int r, int L, int R, int val) {
    if (L <= l && r <= R) {
        for (int i = 0; i <= 2; ++i) {
            t[i][st].sum = (t[i][st].sum * val) % mod;
            t[i][st].lazM = (t[i][st].lazM * val) % mod;
            t[i][st].lazS = (t[i][st].lazS * val) % mod;//由乘法分配律,lazS也需要乘
        }
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        mul(ls, l, mid, L, R, val);
    if (R > mid)
        mul(rs, mid + 1, r, L, R, val);
    for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
}

void change(int st, int l, int r, int L, int R) {
    if (L <= l && r <= R) {
        lazC[st] = (lazC[st] + 1) % 3;//转换操作转换三次后就等于不转换,所以取模
        node x = t[0][st], y = t[1][st], z = t[2][st];//lazC标记实际含义为保留子节点的信息,所以父节点要进行操作
        t[0][st] = y;
        t[1][st] = z;
        t[2][st] = x;
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        change(ls, l, mid, L, R);
    if (R > mid)
        change(rs, mid + 1, r, L, R);
    for (int i = 0; i <= 2; ++i)t[i][st].sum = (t[i][ls].sum + t[i][rs].sum) % mod;
}

void query(int st, int l, int r, int L, int R, int a[]) {
    if (L <= l && r <= R) {
        for (int i = 0; i <= 2; ++i) {
            a[i] = (a[i] + t[i][st].sum) % mod;
        }
        return;
    }
    push_down(st);
    int mid = (l + r) >> 1;
    if (L <= mid)
        query(ls, l, mid, L, R, a);
    if (R > mid)
        query(rs, mid + 1, r, L, R, a);
}

void solve() {
    //注意本代码已#define int long long
    int n, m;
    cin >> n >> m;
    //为离散化,先读入,后操作
    for (int i = 1; i <= m; ++i) {
        cin >> q[i][0] >> q[i][1] >> q[i][2];
        ++q[i][2];//形成左闭右开区间
        v.push_back(q[i][1]);
        v.push_back(q[i][2]);
        if (q[i][0] == 1) {
            for (int j = 3; j <= 5; ++j)cin >> q[i][j];
        } else if (q[i][0] == 2) {
            cin >> q[i][3];
        }
    }
    v.push_back(LLONG_MIN);//放入一个最小值,保证数组以1开始,也可以开一个数组进行离散化
    sort(v.begin(), v.end());//离散化需要先排序
    v.erase(unique(v.begin(), v.end()), v.end());//将多余的数字去除
    for (int i = 1; i <= m; ++i) {
        //通过二分离散化
        q[i][1] = lower_bound(v.begin(), v.end(), q[i][1]) - v.begin();
        q[i][2] = lower_bound(v.begin(), v.end(), q[i][2]) - v.begin();
    }
    int tot = v.size() - 1;//线段树的叶子结点个数,即操作区间的最右端点
    build(1, 1, tot);
    for (int i = 1; i <= m; ++i) {
        //由于操作左闭右开区间,所以每次操作右端点需要-1
        if (q[i][0] == 1) {
            int x[] = {q[i][3], q[i][4], q[i][5]};
            add(1, 1, tot, q[i][1], q[i][2] - 1, x);
        } else if (q[i][0] == 2) {
            mul(1, 1, tot, q[i][1], q[i][2] - 1, q[i][3]);
        } else if (q[i][0] == 3) {
            change(1, 1, tot, q[i][1], q[i][2] - 1);
        } else {
            int x[] = {0, 0, 0};
            query(1, 1, tot, q[i][1], q[i][2] - 1, x);
            int ans = 0;
            for (int j = 0; j <= 2; ++j) {
                ans = (ans + x[j] * x[j] % mod) % mod;
            }
            cout << ans << \'\\n\';
        }
    }
}

signed main() {
    start;//关同步
    while (T--)
        solve();
    return 0;
}

总结

本题难点在于三个标记的影响以及区间离散化。

对于前两个标记在洛谷OJ中有原题,转换则需要考虑多种组合,以及不正确转换的反例,比如说转换和运算的先后,是否带标记转换还是只转换\\(sum\\),转换根据本节点的标记还是父节点的标记等等。题解给出的是正确的转换方式,但是非正确的转换方式很容易干扰思路,所以请完全理解为什么如此转换。

而区间离散化首先需要理解离散化的思想,其次理解为什么要将区间变为左闭右开区间,最后理解每一个节点代表什么。

ccf历年试题

来源:CCF计算机职业资格网站

CCF201612试题

CCF201609试题

CCF201604试题

CCF201512试题

CCF201509试题

CCF201503试题

CCF201412试题

CCF201409试题

CCF201403试题

CCF201312试题

 

CCF201312--模拟练习试题参考答案(C++)

CCF201312--模拟练习试题参考答案(Java)

以上是关于CCF 202012-5星际旅行(20~100分)的主要内容,如果未能解决你的问题,请参考以下文章

CCF202009-4 星际旅行(100分题解链接)

星际旅行

CCF202006-4 1246矩阵快速幂(100分题解链接)

CCF201503-5 最小花费(100分解题链接)

CCF201812-3 CIDR合并(100分)位运算+文本

CCF认证历年试题