2021年寒假每日一题,2017~2019年的省赛真题。
本文内容由倪文迪(华东理工大学计算机系软件192班)和罗勇军老师提供。
后面的每日一题,每题发一个新博文,请大家每天看博客蓝桥杯专栏: https://blog.csdn.net/weixin_43914593/category_10721247.html
提供C++、Java、Python三种语言的代码。
文章目录
2018省赛A组第7题“三体攻击” ,题目链接:
http://oj.ecustacm.cn/problem.php?id=1364
https://www.dotcpp.com/oj/problem2275.html
1、题目描述
一个
A
×
B
×
C
A×B×C
A×B×C,即A层B行C列的立方体,第i层j行k列(记为战舰
(
i
,
j
,
k
)
(i,j,k)
(i,j,k))的生命值是
d
(
i
,
j
,
k
)
d(i,j,k)
d(i,j,k)。
三体人队它发起m轮“立方体攻击”,每次攻击对一个小立方体的所有战舰都造成同等伤害。具体地,第t轮攻击用7个参数
l
a
t
,
r
a
t
,
l
b
t
,
r
b
t
,
l
c
t
,
r
c
t
,
h
t
la_t, ra_t, lb_t, rb_t, lc_t, rc_t, h_t
lat, rat, lbt, rbt, lct, rct, ht 描述,即所有位于
i
∈
[
l
a
t
,
r
a
t
]
,
j
∈
[
l
b
t
,
r
b
t
]
,
k
∈
[
l
c
t
,
r
c
t
]
i ∈ [la_t, ra_t],j ∈ [lb_t, rb_t],k ∈ [lc_t, rc_t]
i ∈ [lat, rat],j ∈ [lbt, rbt],k ∈ [lct, rct] 的战舰
(
i
,
j
,
k
)
(i, j, k)
(i, j, k) 会受到
h
t
h_t
ht 的伤害。如果一个战舰累计受到的总伤害超过其防御力,那么这个战舰会爆炸。
问第一艘爆炸的战舰是在哪一轮攻击后爆炸的。
数据规模:
A
×
B
×
C
≤
1
0
6
,
m
≤
1
0
6
,
0
≤
d
(
i
,
j
,
k
)
,
h
t
≤
1
0
9
A × B × C ≤ 10^6, m ≤ 10^6, 0 ≤ d(i, j, k), h_t ≤ 10^9
A × B × C ≤ 106, m ≤ 106, 0 ≤ d(i, j, k), ht ≤ 109。
时间限制:2s。
内存限制:256M。
2、题解
(比较深入地搞过算法竞赛的队员一看就知道,这是一个三维差分的模板题。)
首先看看数据规模,有
n
=
1
0
6
n=10^6
n=106个点,
m
=
1
0
6
m=10^6
m=106次攻击,如果用暴力,统计每次攻击后每个点的生命值,那么复杂度是
O
(
m
n
)
O(mn)
O(mn)的,题目时限是2s,必然会超时。
暴力法的编码很简单,可以用来对敲,检验正式代码的正确性。
题目给的是三维空间,我们可以先思考一维、二维情况,看是否有启发。
2.1 一维差分+二分法
一维,即所有战舰排成一条线。
每次把一个区间内的所有元素(战舰生命值)减去一个相同的
h
t
h_t
ht值,是经典的“一维区间修改问题”,可以用“差分数组”来处理数据。
注:“差分数组”的概念,请看博文树状数组的“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、对敲和测试
正好用这个题目练练对敲和测试。
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.out
,结果如下:
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: 找不到差异