“葡萄城杯”牛客周赛 Round 53 D题
小红组比赛
题目背景
“葡萄城杯”牛客周赛 Round 53
题目描述
小红希望出一场题目,但是他的实力又不够,所以他想到可以从以前的比赛中各抽一题,来组成一场比赛。不过一场比赛的难度应该是有限制的,所以所以这一场比赛会给一个目标难度分数 t a r g e t target target
小红选 n n n场比赛,每场 m m m个题,小红会从每一场选一道题,使其组成的题的难度分数尽量接近 t a r g e t target target。小红想知道挑选的题的难度分数与 t a r g e t target target相差的最小值是多少。
输入格式
第一行包含两个正整数 n , m ( 1 ≤ n ≤ 100 , 1 ≤ m ≤ 20 ) n, m(1\le n \le 100 ,1 \le m \le 20) n,m(1≤n≤100,1≤m≤20)代表小红选了 n n n场比赛,每场比赛有 m m m道题。
此后 n n n 行,第 i i i行输入 m m m个整数 a i , j ( 1 ≤ a i , j ≤ 50 ) a_{i,j}(1\le a_{i,j} \le 50) ai,j(1≤ai,j≤50) 代表第 i i i场比赛的第 j j j道题的难度分数。
最后一行输入一个整数 t a r g e t ( 1 ≤ t a r g e t ≤ 5000 ) target (1 \le target \le 5000) target(1≤target≤5000)代表小红希望这一场比赛的难度分数。
输出格式
在一行上输出一个整数,表示挑选的题的难度分数与 t a r g e t target target相差的最小值。
样例 #1
样例输入 #1
3 3
1 4 7
2 5 8
3 6 9
10
样例输出 #1
1
说明
小红可以选第一场比赛的第一道题,第二场比赛的第一道题,第三场比赛的第二道题,这样挑选的题的难度分数为 1 + 2 + 6 = 9 1+2+6=9 1+2+6=9,与 t a r g e t target target相差的最小值为 1 1 1。
做题要点
- 小红会从每一场选一道题
- 组成的题的难度分数尽量接近 t a r g e t target target
- 1 ≤ n ≤ 100 , 1 ≤ m ≤ 20 1\le n \le 100 ,1 \le m \le 20 1≤n≤100,1≤m≤20
做题思路
因为 1 ≤ n ≤ 100 , 1 ≤ m ≤ 20 1\le n \le 100 ,1 \le m \le 20 1≤n≤100,1≤m≤20,如果用普通的dfs去遍历枚举每一种情况的话,时间复杂度将会来到 O ( m n ) O(m^n) O(mn),必定是不可以的,而且数据严苛的话,很难通过剪枝优化去大幅度削减时间复杂度。
第一个思考起点基于分治法。
分治策略为递归地求解一个问题,每层递归中有三个步骤:
- 分解步骤将问题分为一些子问题,子问题的形式和原问题一样,只是规模更小
- 解决步骤递归地求解出子问题,如果子问题的规模足够小,则停止递归,直接求解。
- 合并步骤将子问题组合成原问题的解。
那么应用在这一题原问题选择 n n n个题目,分为子问题的话就是选择 n − 1 n-1 n−1个题目。因为目标不好分为子问题,但看到 m m m的规模小和 t a r g e t < = 5000 target<=5000 target<=5000,如果能列出子问题的所有情况,就可以得出答案。
根据输入样例
1 4 7
2 5 8
3 6 9
子问题的规模足够小也就意味着选择一个题目。
选择一个题目有 1 , 4 , 7 1,4,7 1,4,7三种
那么选择两个题目实际上就在这三种情况上分别加上
2
,
5
,
8
2,5,8
2,5,8
也就变为
1
+
2
,
4
+
2
,
7
+
2
,
1
+
5
,
4
+
5
,
7
+
5
,
1
+
8
,
4
+
8
,
7
+
8
1+2,4+2,7+2,1+5,4+5,7+5,1+8,4+8,7+8
1+2,4+2,7+2,1+5,4+5,7+5,1+8,4+8,7+8这九种情况(考虑重复)
那么选择三个题目实际上在这九种情况上再分别加上 3 , 6 , 9 3,6,9 3,6,9
以上是步骤可以称为分治策略里的合并步骤。
第二个思考起点基于迭代法
上述的分治有分有合,但实际情况来说思路可以分解,实际操作不用分解。
选择一个题目所有的情况得出后,就可以直接基于选择一个题目的情况得到选择两个题目的所有情况。
以此类推 n ≤ 100 n\le 100 n≤100,也就是说最多迭代100次就可以出结果。
其中每次只需要在前面的情况加上最多 m ≤ 20 m \le 20 m≤20个数字。
有一个问题就在于如果不知道 t a r g e t < = 5000 target<=5000 target<=5000且$a_{i,j}\le 50 $那么就无法限制住情况的个数。
在上面的例子可以得到 m = 3 , n = 2 m=3,n=2 m=3,n=2的时候就可能有九种不重复情况,如果不限制进行迭代,第 k k k次迭代(选择 k k k个题目)的时候就可能出现 m k m^k mk个不重复的情况。在时间复杂度的考虑上和普通的dfs一样糟糕。
而题目说了
t
a
r
g
e
t
<
=
5000
target<=5000
target<=5000且
a
i
,
j
≤
50
a_{i,j}\le 50
ai,j≤50,意味着如果超过
5000
5000
5000的情况就可以舍去了。原因:
a
i
,
j
≤
50
a_{i,j}\le 50
ai,j≤50且
n
≤
100
n\le 100
n≤100 , 那么
∑
i
=
1
n
max
j
=
1
m
a
i
,
j
≤
50
×
100
\displaystyle\sum_{i=1}^{n}\displaystyle\max_{j=1}^{m}a_{i,j} \le 50\times 100
i=1∑nj=1maxmai,j≤50×100。
所以每次迭代的范围就定在了 0 到 5000 0到5000 0到5000。
第三思考起点基于动态规划(dp)
范围定了,思路也有了,那么如何去编程是重点。
因为每次迭代都基于上一个“子问题”的情况。
所以要一个记录情况的数组。因为范围已经确定了,所以开个定长的数组即可。
这里给出的方案为开足够长( > 5000 \gt 5000 >5000)的二维布尔数组。
第一个纬度表示迭代次数/选择题目的个数,第二个纬度表示难度和。
即 b i , j = t r u e b_{i,j} = true bi,j=true表示选择了 i i i个题目使得难度和 j j j的情况存在。
每次每次迭代都基于上一个“子问题”的情况并且已知选择第
i
i
i次的题目难度
那么如果
b
i
−
1
,
k
−
a
i
,
j
=
t
r
u
e
b_{i-1,k-a_{i,j}} = true
bi−1,k−ai,j=true,那么
b
i
,
k
=
t
r
u
e
b_{i,k} = true
bi,k=true
其含义为,如果选择
i
−
1
i-1
i−1个题目使得难度和
k
−
a
i
,
j
k-a_{i,j}
k−ai,j的情况存在,那么在第
i
i
i次选择中选择难度为
a
i
,
j
a_{i,j}
ai,j难度的题目,就可以使得选择
i
i
i个题目难度和
a
i
,
j
a_{i,j}
ai,j的情况存在
在动态规划中 b i , j b_{i,j} bi,j称为状态,而 b i , k ∣ = b i − 1 , k − a i , j b_{i,k} |= b_{i-1,k-a_{i,j}} bi,k∣=bi−1,k−ai,j称为状态转移方程(符号 ∣ | ∣表示按位或)。
一道题没选的时候难度和只能是0,所以 b 0 , 0 = t r u e b_{0,0} = true b0,0=true为初始状态
基于滚动数组的二次优化(空间复杂度上的)
因为状态转移方程只涉及到两组状态,分别是上一次迭代和本次迭代,所以可以只开两个足够长一维布尔数组。然后轮流迭代记录即可实现本题。
总思路
- 初次迭代为什么都不选,即 b 0 , 0 = t r u e b_{0,0} = true b0,0=true
- 每次迭代, b i , k ∣ = b i − 1 , k − a i , j b_{i,k} |= b_{i-1,k-a_{i,j}} bi,k∣=bi−1,k−ai,j
- 最后得出选完 n n n个题目所有难度情况,并选出与 t a r g e t target target差值最小的
核心代码对应思路
memset(b,false,sizeof(b));
b[0][0] = true;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=a[i][j];k<=5050;k++){
b[i][k] |= b[i-1][k-a[i][j]];
}
}
}
- 初次迭代为什么都不选,即 b 0 , 0 = t r u e b_{0,0} = true b0,0=true
- 每次迭代, b i , k ∣ = b i − 1 , k − a i , j b_{i,k} |= b_{i-1,k-a_{i,j}} bi,k∣=bi−1,k−ai,j
- 最后得出选完 n n n个题目所有难度情况,并选出与 t a r g e t target target差值最小的
迭代
n
n
n次,每次有
m
m
m个选择
其中迭代范围为
0
到
5000
0到5000
0到5000。(这里做题的时候没有细细验证所以定到了5050)
注意:
k
k
k的起点为
a
i
,
j
a_{i,j}
ai,j是因为状态转移方程含有
k
−
a
[
i
]
[
j
]
k-a[i][j]
k−a[i][j],只有
k
−
a
[
i
]
[
j
]
≥
0
k-a[i][j] \ge 0
k−a[i][j]≥0才有意义,所以当
k
=
a
[
i
]
[
j
]
k = a[i][j]
k=a[i][j]时候符合范围条件。
时间复杂度分析
读入时间复杂度为
O
(
n
×
m
)
O(n\times m)
O(n×m)
迭代过程时间复杂度为
O
(
5000
n
×
m
)
O(5000n\times m)
O(5000n×m)
总时间复杂度约为
1
0
7
10^7
107
伪代码
代码
#include <iostream>
#include <vector>
#include <queue>
#include <tuple>
#include <map>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e3+10;
const int INF = 0x3f3f3f3f;
int n,m,target;
int a[N][N];
bool b[110][(int)5e3+50];
int ans = INF,now=0;
void init(){
}
int main(){
init();
cin >> n >> m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin >> a[i][j];
cin >> target;
memset(b,false,sizeof(b));
b[0][0] = true;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
for(int k=a[i][j];k<=5050;k++){
b[i][k] |= b[i-1][k-a[i][j]];
}
}
}
for(int k=0;k<=5050;k++){
if(b[n][k])
ans = min(ans,abs(k-target));
}
cout << ans;
return 0;
}