【2018.10.18】noip模拟赛Day2 地球危机(2018年第九届蓝桥杯C/C++A组省赛 三体攻击)

题目描述

三体人将对地球发起攻击。为了抵御攻击,地球人派出了 $A × B × C$ 艘战舰,在太 空中排成一个 $A$ 层 $B$ 行 $C$ 列的立方体。其中,第 $i$ 层第 $j$ 行第 $k$ 列的战舰(记为战舰 $d(i, j,k)$)的生命值为 $d_{i, j,k}$。

三体人将会对地球发起 m 轮“立方体攻击”,每次攻击会对一个小立方体中的所有 战舰都造成相同的伤害。具体地,第 t 轮攻击用 7 个参数 $la_t ,ra_t , lb_t ,rb_t , lc_t ,rc_t , h_t$ 描述; 所有满足 $i ∈ [la_t ,ra_t], j ∈ [lb_t,rb_t], k ∈ [lc_t,rc_t]$ 的战舰 $(i, j, k)$ 会受到 $h_t$ 的伤害。如果一个 战舰累计受到的总伤害超过其生命值,那么这个战舰会爆炸。

地球指挥官希望你能告诉他,第一艘爆炸的战舰是在哪一轮攻击后爆炸的。

输入格式

从文件 attack.in 中读入数据。

第一行包括 4 个正整数 $A,B,C,m$;

第二行包含 $A × B × C$ 个整数,其中第 $((i − 1) × B + (j − 1)) × C + (k − 1) + 1$ 个数 为 $d_{i,j,k}$;

第 3 到第 $m + 2$ 行中,第 $t − 2$ 行包含 $7$ 个正整数 $la_t ,ra_t , lb_t ,rb_t , lc_t ,rc_t , h_t$。

输出格式

输出到文件 attack.out 中。

输出第一个爆炸的战舰是在哪一轮攻击后爆炸的。保证一定存在这样的战舰。

样例 1 输入

2 2 2 3
1 1 1 1 1 1 1 1
1 2 1 2 1 1 1
1 1 1 2 1 2 1
1 1 1 1 1 1 2

样例 1 输出

2

样例 1 解释

在第 $2$ 轮攻击后,战舰 $(1, 1, 1)$ 总共受到了 $2$ 点伤害,超出其防御力导致爆炸。

子任务

对于 $10\%$ 的数据,$B = C = 1$;

对于 $20\%$ 的数据,$C = 1$;

对于 $40\%$ 的数据,$A × B × C, m ≤ 10, 000$;

对于 $70\%$ 的数据,$A,B,C ≤ 200$;

对于所有数据,$A × B × C ≤ 10^6 , m ≤ 10^6 , 0 ≤ d_{i, j,k},h_t ≤ 10^9$。

 

题解

讲道理,这应该是网上第一篇详细讲这题的题解了。

之前翻这题题解的时候,大部分人都“只打了过70分的暴力”(实际上是50分复杂度的),好像还有人说若有时间复杂度更低的做法,请指教……

那这就是他需要的做法了吧。

 

50pts

随便$O(ABC*m)$的暴力就行。

 

100pts

首先说一下,满分做法不需要数据结构,更不需要什么三维线段树,应该没超过省选难度。

但是比较难想,亲自做一下就能体验到。

这种题显然是有单调性的,即飞船不会加血,如果它在某一秒被打到0血以下,那它以后就一直在0血以下了。

所以可以二分答案(攻击次数),然后判断此时是否存在血量$\lt 0$ 的飞船,如果有就降低答案的二分范围,没有就升高答案的二分范围。

虽然存在单调性,但二分判断好像还得暴力进行每次攻击操作,于是二分就没用了。

简单地说,要让二分上场,我们就得用$O(m)$的时间求出$m$次操作后所有飞船剩余的血量。

三维区间减法的不好想,可以先降为一维。

一维区间减法除了用段树之外,还有什么方法维护?

差分!

差分具体是个什么东西?

给你一个序列$a$,设一个序列$s$为$a$序列中相邻两个数的差,即$s(i)=a(i)-a(i-1)$。特别的,未赋值的$a(0)$为$0$。

我们发现,这样一个数就可以用前面所有的差的和来表示,即$a(i)\space =\space a(i)-a(i-1)+a(i-1)-a(i-2)+...+a(1)-a(0)\space =\space s(i)+s(i-1)+...+s(1)$

这就是差的前缀和。

如果我们只想快速得到少量位置的当前数的话,应该用树状数组维护差分。

但我们现在需要知道所有的数(判断是否存在小于$0$的数),就没必要再用一个$log$维护了,直接线性递推$s(i)$求出每个$a(i)$。

这样有什么好处呢?

当进行区间减法(区间加法同理)时,区间内的数彼此的相对差是不变的(都减少了同一个值,差没变),只有两端的差变了。所以每次区间减法只需要修改两端的差分值。

比如有个数列$a$:1 2 3 2 3 1

对应的差分值$s$:1 1 1 -1 1 -2

现在让$[3,5]$区间都$-1$。

只有$2,3$和$5,6$的差变了,$a(3)-a(2)$减少了$1$,$a(6)-a(5)$增加了$1$。

于是直接修改第$3$和第$6$个位置的差分值。

 

难度升级,考虑二维。

二维的差分变化情况就是这样

 

比如一次操作要把图中四个点形成的矩形区间的所有值$-1$,那每个角点的差分值变化就是点旁标的数字。

 

再推广到三维,就是这样(我还亲自连了线……)

比如一次操作要对图中八个点形成的立体区间的所有值$-1$,那每个角点的差分值变化就是点旁标的数字。

其实,我们会发现一个规律,就是相邻的两个点的差分值一定是相反的。

原因很简单:由于是区间运算,不管在哪一维度,只要前面有一个点的差分值增大或减小,后面就一定有一个点把差分值补回去,这样才能保证不修改到区间后的数。

所以可以用类似二分图匹配的方法计算多维差分时,操作空间的每个顶点的差分值的正负。这里只有三维,所以可以直接赋。

三维差分计算每个位置的当前数的方法:递推求以 $(0,0,0)$ 为左下角,当前点为右上角的立方体中所有位置的差分和。

这样的时间复杂度是$O(维度*空间大小)$的,对于本题就是$O(3*ABC)$。

 

以上是用$O(m)$的时间求出$m$次操作后所有飞船剩余的血量的方法。这种方法只能快速求出一定次操作后的差分值,对于不同的操作次数,得到每个位置的数所需的时间复杂度很高(得暴力跑一遍$A*B*C$的矩阵),所以不能过多次得到整个矩阵当前每个位置的数(暴力是能的)。因此套用二分就得到了与暴力不同的高效做法。

总结一下,就是对于前$m$个操作,每个操作都修改8个位置的差分值,都操作完后多维差分计算每个位置的数,最后暴力判断每个数是否$\lt 0$ 即可。(只要找到一个就可以退出$check$)

外面套二分,时间复杂度$O(log(m)*(m+ABC))$。

 

时间复杂度如何优化到理论$O(m+ABC)$

首先,我们的二分有特殊性质,即相邻两次二分的答案的变化量依次为 $\frac{m}{2},\frac{m}{4},\frac{m}{8}$,以此类推。

图中的蓝色编号为二分答案顺序,下面的彩色细线为移动长度。

由于这题比较特殊,二分的是攻击次数,所以我们不必每次二分都重新差分前面的攻击操作,造成大量重复计算,而直接在原差分值的基础上修改二分答案的变化量次即可。

根据调和级数,这样的总差分次数是 $\frac{m}{2}+\frac{m}{4}+\frac{m}{8}+...\le 1$,所有差分修改的总时间复杂度就变成了$O(m)$。

如果你不理解调和级数,我这里有个通俗易懂的解释能证明调和级数:把上式结果转为二进制,由于每个加数都把互不相同的一位都加了$1$,表示出来就是$0.1111...$(小数位可以有任意多个$1$),可以看出它无限接近但不等于$1$。

这个优化是可以实际操作的,于是时间复杂度变成了$O(m+log(m)*ABC)$。

 

那$ABC$呢?

可以再考虑一下这题的特征:任意飞船血量是单调不上升的。

这就说明,对于二分出的一个攻击次数$m$,如果存在血量$\lt 0$ 的飞船,我们肯定要降低答案的二分范围,而此时那些血量$\ge 0$ 的飞船在减少被攻击次数后血量肯定还$\ge 0$,依然不可能成为答案,所以我们可以用链表存储飞船矩阵,然后在上述情况时删掉这些位置。

但实际上三维链表很难写,所以最多记个$vis$数组记录被删掉的位置。

而且这个优化仅限于理论,实际上它的优化效果是不定的,时间复杂度最优是$O(m+ABC)$,最坏还是$O(m+log(m)*ABC)$(正好要进行完$m$次攻击后才能出现一个挂掉的飞船时,上述优化就没法用)。

 

代码是$syf$大佬的 $AC\space code$,格式嘛……

 1 #include<bits/stdc++.h>
 2 #define rep(i,x,y) for(register int i=(x);i<=(y);i++)
 3 #define dwn(i,x,y) for(register int i=(x);i>=(y);i--)
 4 #define maxn 1000010
 5 #define LL long long
 6 using namespace std;
 7 int read()
 8 {
 9     int x=0,f=1;char ch=getchar();
10     while(!isdigit(ch)&&ch!='-')ch=getchar();
11     if(ch=='-')f=-1,ch=getchar();
12     while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
13     return x*f;
14 }
15 void write(int x)
16 {
17     int f=0;char ch[20];
18     if(!x){putchar('0'),putchar('\n');return;}
19     if(x<0)x=-x,putchar('-');
20     while(x)ch[++f]=x%10+'0',x/=10;
21     while(f)putchar(ch[f--]);
22     putchar('\n');
23 }
24 LL qh[maxn],a[maxn],s[maxn],d[maxn];
25 int qxa[maxn],qya[maxn],qza[maxn],qxb[maxn],qyb[maxn],qzb[maxn],A,B,C,m,ans;
26 int getx(int x,int y,int z){return (x*B+y)*C+z+1;}
27 void getp(int x,int px,int py,int pz){x--;pz=x%C,x/=C,py=x%B,pz=x/B;return;}
28 void add(int x,int y,int z,LL h){if(x<=(A-1)&&y<=(B-1)&&z<=(C-1))a[getx(x,y,z)]+=h;}
29 void atk(int xa,int ya,int za,int xb,int yb,int zb,LL h)
30 {
31     add(xa,ya,za,h),add(xa,ya,zb+1,-h),add(xa,yb+1,za,-h),add(xa,yb+1,zb+1,h);
32     add(xb+1,ya,za,-h),add(xb+1,ya,zb+1,h),add(xb+1,yb+1,za,h),add(xb+1,yb+1,zb+1,-h);
33 }
34 int check()
35 {
36     rep(i,0,A-1)rep(j,0,B-1)rep(k,0,C-1)s[getx(i,j,k)]=a[getx(i,j,k)];
37     if(C!=1)rep(i,0,A-1)rep(j,0,B-1)rep(k,1,C-1)s[getx(i,j,k)]+=s[getx(i,j,k-1)];//,cout<<i<<" "<<j<<" "<<k<<"+1 "<<s[getx(i,j,k-1)]<<endl;
38     if(B!=1)rep(i,0,A-1)rep(j,1,B-1)rep(k,0,C-1)s[getx(i,j,k)]+=s[getx(i,j-1,k)];//,cout<<i<<" "<<j<<" "<<k<<"+2 "<<s[getx(i,j-1,k)]<<endl;
39     if(A!=1)rep(i,1,A-1)rep(j,0,B-1)rep(k,0,C-1)s[getx(i,j,k)]+=s[getx(i-1,j,k)];//,cout<<<<"+3 "<<s[getx(i-1,j,k)]<<endl;
40     rep(i,0,A-1)rep(j,0,B-1)rep(k,0,C-1)if(s[getx(i,j,k)]>d[getx(i,j,k)])return 1;
41     return 0;
42 }
43 void getans(int L,int R,int lastmid)
44 {
45     if(L>R)return;
46     int mid=(L+R)>>1;
47     if(mid>lastmid)rep(i,lastmid+1,mid)/*cout<<"mid:"<<mid<<" add:"<<i<<endl,*/atk(qxa[i],qya[i],qza[i],qxb[i],qyb[i],qzb[i],qh[i]);
48     if(mid<lastmid)rep(i,mid+1,lastmid)/*cout<<"mid:"<<mid<<" del:"<<i<<endl,*/atk(qxa[i],qya[i],qza[i],qxb[i],qyb[i],qzb[i],-qh[i]);
49     int f=check();
50     //cout<<"mid:"<<mid;
51     //cout<<"now:";
52     //rep(i,0,A-1)rep(j,0,B-1)rep(k,0,C-1)cout<<s[getx(i,j,k)]<<" ";cout<<endl;
53     if(f){ans=min(ans,mid);getans(L,mid-1,mid);}
54     else getans(mid+1,R,mid);
55 }
56 int main()
57 {
58     //freopen("attack.in","r",stdin);
59     //freopen("attack.out","w",stdout);
60     A=read(),B=read(),C=read(),ans=m=read();
61     rep(i,0,A-1)rep(j,0,B-1)rep(k,0,C-1)d[getx(i,j,k)]=read();
62     rep(i,1,m)qxa[i]=read()-1,qxb[i]=read()-1,qya[i]=read()-1,qyb[i]=read()-1,qza[i]=read()-1,qzb[i]=read()-1,qh[i]=read();
63     getans(1,m,0);
64     write(ans);
65     return 0;
66 }
67 /*
68 2 2 2 3
69 1 1 1 1 1 1 1 1
70 1 2 1 2 1 1 1
71 1 1 1 2 1 2 1
72 1 1 1 1 1 1 2
73 */
View Code

 

posted @ 2018-10-18 20:02  大本营  阅读(4097)  评论(2编辑  收藏  举报