HDU 4872 ZCC Loves COT 对标记的标记打标记

原题:http://acm.hdu.edu.cn/showproblem.php?pid=4872

题意:给一个n*n*n的方体,定义两种操作,修改操作是给出x,y,z,a,把满足0 <= k <= j <= i < a的所有val[x+i][y+j][z+k]的值+1,询问操作类似询问这个区域的val和,更新和查询都是10W,n是100,所有更新都结束后才会开始查询


解题思路:

首先请看此题的一维版本:http://acm.hdu.edu.cn/showproblem.php?pid=1556

这道题目就很经典了,对于一次更新[l, r], 我们操作 a[l]++, a[r+1]-- ,然后求一次前缀和,就可以得到具体的 a[i] 的值

原理就是遇到+1表示有一次涂色从这里开始,遇到-1表示有一次涂色到此结束,具体就不在详细说了

对于这样的操作我们称为打标记


然后我们把他扩展到二维版本

定义修改操作是给出 x, y, a 对满足 0 <= j <= i < a 的所有 val[x+i][y+j] 的值 +1, 询问类似

我们在平面首先画出这样一个区域

HDU 4872 ZCC Loves COT 对标记的标记打标记_第1张图片

向下的是x + i的部分,向右是y + j部分,满足 j <= i 的部分画出来就是这样一个三角形

那么我们仿照一维的情况,对于每一行打标记,就变成了下面这样

HDU 4872 ZCC Loves COT 对标记的标记打标记_第2张图片

这时我们可以发现,标记是连续的一段,相当于我们对一维的线段打的两个点标记,在线段扩展到二维平面的同时,被拉伸成了线段

那么对于这个由标记生成的线段,我们仍然可以打标记,对于两个方向,分别打标记,一共四个,效果如下

HDU 4872 ZCC Loves COT 对标记的标记打标记_第3张图片

竖直方向的+1, -1是每行 + 标记的标记,斜线方向的则是 - 的标记

注意 - 标记也是继续按 +1 -1来打标记,这时的前缀和表示有多少个 - 标记,而不是减了多少

然后对于这两种标记,我们分别先沿向下和向右下的方向做一次前缀和,就可以还原出一维标记的情况

然后再从左向右累加,就可以得到具体元素的值


那么当我们得到具体元素的值之后,如何在O(1)效率求解任意三角形区域的sum值,还需要进一步讨论

根据一维前缀和,我们可以知道要想O(1)求解,必需通过类似前缀相减的方式,那么前缀本身不一定是线性的,只要他沿着若干方向一直扩展到边界,就可以是认为是前缀和

我们先讨论一个比较特殊的情况,就是询问的 x == y,如下图


Q点是我们询问的坐标(x, y), △QAD的区域就是我们要求sum值,通过一阵脑洞可以发现

△QAD = △AOB - △ QOC - 四边形CBDQ

而 四边形CBDQ = 四边形OBDE - 四边形OCQE

转化到这一步,所有的变量我们都可以通过一定姿势的前缀和求得了,我们用 tri[][] 来表示从点到x轴和原点构成的三角形区域,如 tri[A.x][A.y] = △AOB

再用 rec[][] 表示点到x轴y轴构成的区域,如 rec[D.x][D.y] = 四边形OBDE

则有 △QAD = tri[A] - tri[Q] - (rec[D] - rec[Q]) , tri和rec的预处理就很容易了,借助一下每行的前缀 pre[][]即可


另外一种情况就是 x != y, 这个时候和上面不一样的地方就是AQ延长线不过原点,那么图形不规则,似乎不符合我们上面的规律

但实际上由于外面区域都为0,所以可以假象扩大坐标系,把他弄到原点去,所以我们预处理前缀时只要按方向累加即可,不需要考虑太多


下面是将本题退化到二维情况的代码:

部分数组的含义如上文所示

其他的如 add[][] 表示一维 + 标记的再标记,sub为一维 - 标记的再标记

由于for循环的嵌套过于丧心病狂(主要是三维的时候),于是我第一次define了for循环,REP什么的都比较常见了

<span style="font-family:Courier New;">//
#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdlib>
using namespace std;

#define LL long long
#define REP(i,a,b) for(int i = a; i <= b; ++i)

typedef LL Node[105][105];

Node val, add, sub, pre, tri, rec;

void init() {
    memset(add, 0, sizeof(add));
    memset(sub, 0, sizeof(sub));
}

void upd(int x, int y, int a) {
    ++add[x][y];
    --add[x + a][y];
    ++sub[x][y + 1];
    --sub[x + a][y + a + 1];
}

void solve(int n) {
    REP(i,1,n) REP(j,1,n) {
        add[i][j] += add[i - 1][j];
        sub[i][j] += sub[i - 1][j - 1];
        val[i][j] = val[i][j - 1] + add[i][j] - sub[i][j];
        pre[i][j] = pre[i][j - 1] + val[i][j];
        rec[i][j] = rec[i - 1][j] + pre[i][j];
        tri[i][j] = tri[i - 1][j - 1] + pre[i][j];
    }
}

LL cal(int x, int y, int a) {
    --x, --y;
    LL ans = 0;
    ans += tri[x + a][y + a] - tri[x][y];
    ans -= rec[x + a][y] - rec[x][y];
    return ans;
}

int main()
{
    int n, m, q, x, y, a;
    while( scanf( "%d%d%d", &n, &m, &q ) != EOF ) {
        init();
        while( m-- ) {
            scanf( "%d%d%d", &x, &y, &a );
            upd(x, y, a);
        }
        solve(n);
        while( q-- ) {
            scanf( "%d%d%d", &x, &y, &a );
            printf( "%lld\n", cal(x, y, a) );
        }
    }
    return 0;
}</span>


那么下面我们继续讨论扩展到三维的情况:

友情提示请先弄清楚二维的情况,并自备墨镜,下面的图形将开始各种丧心病狂


首先我们根据一维到二维的情况,一维的标记扩展后被拉伸成了线段,那么二维的标记在扩展到三维的情况时,是不是也一样是拉伸成线段?

这个不难想通,把上面那个二维标记的平面,沿z轴向上拉伸,三角形变成四面体,四个标记变成了四个线段

实质上把拉伸后的每一层(垂直 z 轴切下来一片),他和上面二维的情况是完全一致的,只是三角形的大小在变化,图形如下

HDU 4872 ZCC Loves COT 对标记的标记打标记_第4张图片

这个图形是在更新点Q展开的坐标系,不是从原点,四面体的性质可以这样考虑

原来是一个方体,为了保证i >= j,垂直XOY平面沿x == y切一刀,同理垂直YOZ平面对y == z切一刀,就行了

首先我们考虑QAB那个三角形,其实就是二维那个三角形,然后把这个三角形像C拉伸,那些标记也沿着拉伸方向变成线段

所以我们可以在那四个标记的位置再打标记 +1,在C点附近四个线段结束的地方再打标记 -1

具体的二维标记拉伸出来的效果我就不画了,沿着箭头方向一路写过去就行,你们脑补一下....


然后我们从这四个方向前缀和一下(当然我们又得开4个数组来记录这四条线段的信息), 就可以还原二维的标记add 和 sub数组,然后z那一位就搞定了,问题退化成二维状态

我之后的代码分别用A, B, C, D 四个数组来处理,如果你想通了二维的情况,这里就不难理解了,否则基本GG思密达


接下来我们再讨论怎么O(1)求这个四面体的Sum(val)

根据我们二维是得到的结论,我们用特殊图形(x == y)讨论出的规律,对任意情况适用,所以我们这里仍然使用(x == y == z)的特殊点来讨论

下面这个图比较残暴

HDU 4872 ZCC Loves COT 对标记的标记打标记_第5张图片

我们要求的就是 四面体QADE

首先我们看到那个显眼的点A,可以想(nao)到(bu)应该用 四面体OABC - 四面体OPQR

所以我们用 tet[A] 来保存类似四面体OABC的前缀

然后我们还需要减去 三棱柱DCF-QPR 和 三棱柱QDE-RFB

对于三棱柱DCF-QPR 我们显然可以通过保存到YOZ平面的三棱柱的总和 psm[][][],用psm[D] - psm[Q]来获得这一部分的值

那么还剩下 三棱柱QDE-RFB,你看到RFB那个三角形,就差不多能联想到二维那个图形了,是一样的

RFB = OBC - ORP - (PCFR) ,这里PCFR怎么来也很显然,把这四个前缀都加一维就可以了

于是我们需要四类前缀来获得QADE的值,当然预处理这四类前缀还需要其他的辅助数组


最终代码:

部分上文提到的变量就不说了

pre[i][j][k] 保存 val[i][j][1 ~ k], sum[][][]保存类似QPR那样的三角形片,用来再沿x方向累加得到psm, tet什么的

tot[][][] 用了保存需要用到的那个长方体

(PS:这一部分还是弄懂了自己推好点,用到的变量名太多了,英语渣的表示压力很大)

<span style="font-family:Courier New;">//
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

#define LL long long
#define REP(i,a,b) for(int i = a; i <= b; ++i)

typedef LL Node[105][105][105];

Node val, add, sub, A, B, C, D;
Node pre, sum, tot, tri, rec, psm, tet;

void init() {
    memset(A, 0, sizeof(A));
    memset(B, 0, sizeof(B));
    memset(C, 0, sizeof(C));
    memset(D, 0, sizeof(D));
}

void upd(int x, int y, int z, int a) {
    ++A[x][y][z];
    --A[x + a][y + a][z + a];
    ++B[x + a][y][z];
    --B[x + a][y + a][z + a];
    ++C[x][y + 1][z];
    --C[x + a][y + a + 1][z + a];
    ++D[x + a][y + a + 1][z];
    --D[x + a][y + a + 1][z + a];
}

void solve(int n) {
    REP(i,1,n) REP(j,1,n) REP(k,1,n) {
        A[i][j][k] += A[i - 1][j - 1][k - 1];
        B[i][j][k] += B[i][j - 1][k - 1];
        C[i][j][k] += C[i - 1][j - 1][k - 1];
        D[i][j][k] += D[i][j][k - 1];
    }
    REP(i,1,n) REP(j,1,n) REP(k,1,n) {
        add[i][j][k] = A[i][j][k] - B[i][j][k];
        add[i][j][k] += add[i - 1][j][k];
        sub[i][j][k] = C[i][j][k] - D[i][j][k];
        sub[i][j][k] += sub[i - 1][j - 1][k];
        val[i][j][k] = val[i][j - 1][k] + add[i][j][k] - sub[i][j][k];
    }
    REP(i,1,n) REP(j,1,n) REP(k,1,n) {
        pre[i][j][k] = pre[i][j][k - 1] + val[i][j][k];
        sum[i][j][k] = sum[i][j - 1][k - 1] + pre[i][j][k];
        tot[i][j][k] = tot[i][j - 1][k] + pre[i][j][k];
        tri[i][j][k] = tri[i - 1][j - 1][k] + tot[i][j][k];
        rec[i][j][k] = rec[i - 1][j][k] + tot[i][j][k];
        psm[i][j][k] = psm[i - 1][j][k] + sum[i][j][k];
        tet[i][j][k] = tet[i - 1][j - 1][k - 1] + sum[i][j][k];
    }
}

LL cal(int x, int y, int z, int a) {
    --x, --y, --z;
    LL ans = 0, tmp = 0;
    ans += tet[x + a][y + a][z + a] - tet[x][y][z];
    ans -= psm[x + a][y][z] - psm[x][y][z];
    tmp += tri[x + a][y + a][z] - tri[x][y][z];
    tmp -= rec[x + a][y][z] - rec[x][y][z];
    ans -= tmp;
    return ans;
}

int main()
{
    int n, m, q, x, y, z, a;
    while( scanf( "%d%d%d", &n, &m, &q ) != EOF ) {
        init();
        while( m-- ) {
            scanf( "%d%d%d%d", &x, &y, &z, &a );
            upd(x, y, z, a);
        }
        solve(n);
        while( q-- ) {
            scanf( "%d%d%d%d", &x, &y, &z, &a );
            printf( "%lld\n", cal(x, y, z, a));
        }
    }
    return 0;
}</span>



你可能感兴趣的:(HDU 4872 ZCC Loves COT 对标记的标记打标记)