題目描述
三體人將對地球發起攻擊。為了抵御攻擊,地球人派出了 $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 */
