倪文迪陪你學藍橋杯2021寒假每日一題:1.19日(2018省賽A組第7題)
2021年寒假每日一題,2017~2019年的省賽真題。
本文內容由倪文迪(華東理工大學計算機系軟體192班)和羅勇軍老師提供。
後面的每日一題,每題發一個新博文,請大家每天看部落格藍橋杯專欄: https://blog.csdn.net/weixin_43914593/category_10721247.html
提供C++、Java、Python三種語言的程式碼。
@
2018省賽A組第7題“三體攻擊” ,題目連結:
https://www.dotcpp.com/oj/problem2275.html
1、題目描述
一個\(A×B×C\),即A層B行C列的立方體,第i層j行k列(記為戰艦\((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]\)
問第一艘爆炸的戰艦是在哪一輪攻擊後爆炸的。
資料規模:\(A × B × C ≤ 10^6, m ≤ 10^6, 0 ≤ d(i, j, k), h_t ≤ 10^9\)。
時間限制:2s。
記憶體限制:256M。
2、題解
(比較深入地搞過演算法競賽的隊員一看就知道,這是一個三維差分的模板題。)
首先看看資料規模,有\(n=10^6\)個點,\(m=10^6\)次攻擊,如果用暴力,統計每次攻擊後每個點的生命值,那麼複雜度是\(O(mn)\)的,題目時限是2s,必然會超時。
暴力法的編碼很簡單,可以用來對敲,檢驗正式程式碼的正確性。
題目給的是三維空間,我們可以先思考一維、二維情況,看是否有啟發。
2.1 一維差分+二分法
一維,即所有戰艦排成一條線。
每次把一個區間內的所有元素(戰艦生命值)減去一個相同的\(h_t\)值,是經典的“一維區間修改問題”,可以用“差分陣列”來處理資料。
注:“差分陣列”的概念,請看博文樹狀陣列的“4. 區間修改 + 單點查詢”、“5、差分陣列”中的講解。請先完全搞懂“差分陣列”,再往下看。
“差分陣列”有什麼好處呢?一次修改一個區間,如果用暴力法,需要修改區間內每個元素的值,複雜度O(n),但是用差分陣列,就只需要修改區間的兩個端點,複雜度O(1)。m次修改的總複雜度只有O(m)。
但是光用差分陣列並不能解決問題。因為在差分陣列上查詢區間內的每個元素是否小於0,需要用差分陣列來計算區間內每個元素的值,複雜度是O(n)的。合起來的總複雜度是O(mn)的,其實跟暴力法的複雜度一樣。
這就要加上第二個演算法:二分法。從第1次修改到第m次修改,肯定有一次修改是臨界點。在這次修改前,沒有負值(戰艦爆炸);在這次修改後,出現了負值,且後面一直有負值。那麼對m進行二分,就能在O(logm)次內找到這個臨界點,這就是答案。
具體的操作步驟是:
1、讀取輸入:儲存n=A × B × C個點(戰艦)的生命值;儲存m次修改。
2、第1次二分,從最大的m開始:判斷做m次修改後是否產生負值。過程是:先做m次差分修改,得到一個差分陣列,複雜度O(m);然後根據這個差分陣列計算每個戰艦的值,看是否有負數,複雜度O(n)。總複雜度O(m+n)。
3、重複以上二分操作,直到找到臨界修改的次數。
一共做O(logm)次二分,總複雜度O((m+n)logm),完美完成編碼任務,AC!
2.2 二維差分、三維差分
同理有二維差分和三維差分。二維差分有4個區間端點;三維差分有8個區間端點。複雜度也都是O((m+n)logm)的,不過常數要大4倍、8倍。
羅老師還沒有寫二維和三維差分的解析。可以參考下面的博文:
二維差分:https://blog.csdn.net/justidle/article/details/104506724
三維差分:
https://blog.csdn.net/weixin_44716674/article/details/105577862
https://blog.csdn.net/weixin_43738764/article/details/105553072
最後是倪文迪的話:“這道題主要考察三維差分以及二分。我們可能處理過二維差分,通過區域性單點的修改以及求字首和來較為高效地求解某一特定時刻的狀態。三維差分則是將對應立方體的八個點修改,原理類似。而本題中二分的是出現負值的id,因為我們只需求解第一次出現負值的編號。”
3、C++程式碼
下面的C++程式碼清晰地重現了上面的解釋。如有疑問,請看註釋。
//cpp檔案取名為 good.cpp
#include<bits/stdc++.h>
using namespace std;
int A,B,C,n,m;
int d[1000005]; //儲存艦隊生命值
int D[1000005]; //三維差分陣列(壓維);同時也用來計算每個點的攻擊值
int lat[1000005],rat[1000005]; //儲存攻擊
int lbt[1000005],rbt[1000005];
int lct[1000005],rct[1000005];
int ht[1000005];
int num(int i,int j,int k) { //小技巧:壓維,把三維座標[i][j][k]轉為一維的((i-1)*B+(j-1))*C+(k-1)+1
if (i>A || j>B || k>C) return 0;
return ((i-1)*B+(j-1))*C+(k-1)+1;
}
bool check(int x) { //檢查經過x次攻擊後是否有戰艦爆炸
for (int i=1; i<=n; i++) D[i]=0;
for (int i=1; i<=x; i++) { //三維差分陣列:三維有8個區間端點
D[num(lat[i], lbt[i], lct[i])] += ht[i];
D[num(rat[i]+1,lbt[i], lct[i])] -= ht[i];
D[num(lat[i], rbt[i]+1,lct[i])] -= ht[i];
D[num(lat[i], lbt[i], rct[i]+1)] -= ht[i];
D[num(rat[i]+1,rbt[i]+1,lct[i])] += ht[i];
D[num(lat[i], rbt[i]+1,rct[i]+1)] += ht[i];
D[num(rat[i]+1,lbt[i], rct[i]+1)] += ht[i];
D[num(rat[i]+1,rbt[i]+1,rct[i]+1)] -= ht[i];
}
for (int i=1; i<=A; i++)
for (int j=1; j<=B; j++)
for (int k=1; k<C; k++)
D[num(i,j,k+1)] += D[num(i,j,k)]; //用差分陣列計算出每個點的攻擊值
for (int i=1; i<=A; i++)
for (int k=1; k<=C; k++)
for (int j=1; j<B; j++)
D[num(i,j+1,k)] += D[num(i,j,k)];
for (int j=1; j<=B; j++)
for (int k=1; k<=C; k++)
for (int i=1; i<A; i++)
D[num(i+1,j,k)] += D[num(i,j,k)];
for (int i=1; i<=n; i++)
if (D[i]>d[i])
return true; //攻擊值大於生命值
return false;
}
int main() {
scanf("%d%d%d%d", &A, &B, &C, &m);
n=A*B*C;
for (int i=1; i<=n; i++) scanf("%d", &d[i]);
for (int i=1; i<=m; i++) scanf("%d%d%d%d%d%d%d",&lat[i],&rat[i],&lbt[i],&rbt[i],&lct[i],&rct[i],&ht[i]);
int L=1,R=m; //經典的二分寫法
while (L<R) { //對m進行二分,找到臨界值
int mid=(L+R)>>1;
if (check(mid)) R=mid;
else L=mid+1;
}
printf("%d\n", R); //列印臨界值
return 0;
}
4、對敲和測試
正好用這個題目練練對敲和測試。
參考博文:Python在競賽中的應用-測試資料的構造與對拍https://blog.csdn.net/weixin_43914593/article/details/111385152
4.1 用python寫個暴力法的對敲程式碼
暴力程式碼很容易寫。下面的程式碼取名為baoli.py
。
A,B,C,m = map(int,input().split())
ship=[]
for i in range(A):
sublist=[]
for j in range(B):
sublist.append([0]*C)
ship.append(sublist)
life=list(map(int,input().split()))
v=0
for i in range(A):
for j in range(B):
for k in range(C):
ship[i][j][k]=life[v] #戰艦生命值
v += 1
num = m
for attacknum in range(1,m+1):
la, ra, lb, rb, lc, rc, ht = map(int,input().split())
for i in range(la-1,ra):
for j in range(lb-1,rb):
for k in range(lc-1,rc):
ship[i][j][k] -= ht
if ship[i][j][k]<0:
print(attacknum)
exit()
4.2 用python產生測試資料
寫一個py檔案,按題目要求的格式產生測試資料。檔名取為test.py
。
程式碼好寫,引數不好設定。如果有讀者有如何設定引數的心得,請告訴我,分享給大家。
#test.py
from random import *
N = 1e4 #自定義一個合適的值
HT = 1e9
A = randint(1,N)
B = randint(1,N//A)
C = randint(1,N//A//B)
m = randint(1,N)
print( A,B,C,m) #第一行
for i in range(A*B*C-1): #第二行
print (randint(HT//1000,HT),end=' ') #生命值設大一些
print (randint(HT//1000,HT))
for i in range(m): #後面m行,每行7個
lat = randint(1,A)
rat = randint(lat,A) #注意:rat比lat大
lbt = randint(1,B)
rbt = randint(lbt,B)
lct = randint(1,C)
rct = randint(lct,C)
ht = randint(1,HT/100000) #攻擊值設小一些
print (lat,rat,lbt,rbt,lct,rct,ht)
5、對拍測試
本機在windows下測試。寫一個迴圈測試對拍的bat檔案,取名為aa.bat
,它的工作是:
先用test.py
產生測試資料,存到data.in
檔案中。
執行對拍程式碼test.py
,讀輸入資料,把輸出資料輸出到檔案py.out
。
執行好程式碼good.cpp
,輸出到檔案good.out
。
用fc
命令比較兩個輸出py.out
和good.out
是否完全一致。
迴圈測試多次。
注意其中的path
是作者機器的目錄,和讀者的path
不同。
@echo off
set path=C:\MinGW\bin
g++ -o good.exe good.cpp
:loop
set path=C:\Users\hp\AppData\Local\Programs\Python\Python39
python test.py >data.in
good.exe <data.in >good.out
python baoli.py <data.in >py.out
set path=C:\Windows\System32
fc py.out good.out
good.exe <data.in
if errorlevel == 1 pause
goto loop
為了看清答案,還列印了每次測試的輸出。在windows的cmd
裡執行aa.bat
,結果如下:
D:\cpp>aa.bat
正在比較檔案 py.out 和 GOOD.OUT
FC: 找不到差異
686
正在比較檔案 py.out 和 GOOD.OUT
FC: 找不到差異
995
正在比較檔案 py.out 和 GOOD.OUT
FC: 找不到差異
1597
正在比較檔案 py.out 和 GOOD.OUT
FC: 找不到差異