[NOIP2007 提高组] 矩阵取数游戏
题目描述
帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的 n × m n \times m n×m 的矩阵,矩阵中的每个元素 a i , j a_{i,j} ai,j 均为非负整数。游戏规则如下:
- 每次取数时须从每行各取走一个元素,共 n n n 个。经过 m m m 次后取完矩阵内所有元素;
- 每次取走的各个元素只能是该元素所在行的行首或行尾;
- 每次取数都有一个得分值,为每行取数的得分之和,每行取数的得分 = 被取走的元素值 × 2 i \times 2^i ×2i,其中 i i i 表示第 i i i 次取数(从 1 1 1 开始编号);
- 游戏结束总得分为 m m m 次取数得分之和。
帅帅想请你帮忙写一个程序,对于任意矩阵,可以求出取数后的最大得分。
输入格式
输入文件包括 n + 1 n+1 n+1 行:
第一行为两个用空格隔开的整数 n n n 和 m m m。
第 2 ∼ n + 1 2\sim n+1 2∼n+1 行为 n × m n \times m n×m 矩阵,其中每行有 m m m 个用单个空格隔开的非负整数。
输出格式
输出文件仅包含 1 1 1 行,为一个整数,即输入矩阵取数后的最大得分。
样例 #1
样例输入 #1
2 3
1 2 3
3 4 2
样例输出 #1
82
提示
【数据范围】
对于
60
%
60\%
60% 的数据,满足
1
≤
n
,
m
≤
30
1\le n,m\le 30
1≤n,m≤30,答案不超过
1
0
16
10^{16}
1016。
对于
100
%
100\%
100% 的数据,满足
1
≤
n
,
m
≤
80
1\le n,m\le 80
1≤n,m≤80,
0
≤
a
i
,
j
≤
1000
0\le a_{i,j}\le1000
0≤ai,j≤1000。
还是想感叹,洛谷这个复制题目makrdown的快捷键是真的舒服,不想leetcode一样需要一点一点的复制
分析
看到这道题我的第一个想法是,这不就是很简单的贪心题吗?
但是,但是,虽然用贪心能过样例,但是只能得20分(两个点)
就拿测试数据2的一行数据为反例:
876 1 566 920 598
所以,在借鉴(白嫖)了很多大佬的方法之后,我总结出来一下一种方法:
- 动态规划
至于为什么只有一种,emmm,我看到的只有这一种
为什么是动态规划?
首先,我们要明白,每一行无论你怎么取都影响不到其他的行,所以动态规划实际上是在每一行的应用
我们可以把每一次拿走边缘的任意一个数的操作分为两种情况:
- 拿左边
- 拿右边
而剩下的区间可以重复如此操作,直到没有数取为止。
所以,倒推回来,我们可以看做:
- 在数组中任取一个数,
当前分值
设为你选的数 - 在你选择的数的左侧或右侧选一个数,使得
当前分值*2 + 所选数的值
最大 - 当前分值则更新为选取的区间的最大分值(即
初始数*2+所选的数
) - 重复2,3步骤直到没有数可以选
此时,我们可以开始规划如何动态规划了:
声明数组f[MAXN][MAXN]
,使f[i][j]
表示,在当前数组中,第i个数到第j个数(闭区间)可以选到的最大分值
而初始值,则有f[i][i] = nums[i]
,只选自己
这样,可以依据上述步骤推出状态转移方程:
f[i][j] = max(nums[i] + f[i + 1][j], nums[j] + nums[i][j - 1]
即选左边或者右边的最大值
而看数据,还是有点小大,需要用到高精度
,但是怎么算最大位数……反正不会超过2^128
,就定40位好了。
定义高精度的类名我本来想用
gj
,但是转念一想,这不是gaoji
的缩写吗,还是用ha (high accuracy)
要好很多。
那么最初代码如下:
/*
* Author: Jeefy Fu
* Email: jeefy163@163.com
* Description:
* Origin URL: https://www.luogu.com.cn/problem/P1005
*/
#include <iostream>
#include <cmath>
#include <fstream>
#include <sstream>
#include <vector>
#include <array>
#include <string>
#include <algorithm>
#include <cstring>
using namespace std;
// high accuracy
// 高精度类
class ha {
public:
int n[41];
// 构造器,创建时清零,防止出现乱七八糟的东西
ha() {
memset(n, 0, sizeof(n));
}
// 主要是为了把int转换为高精度数
ha add(int b) {
ha r;
r.n[0] = b;
int i = 0;
while (r.n[i] != 0) {
r.n[i] += n[i];
r.n[i + 1] += r.n[i] / 10;
r.n[i++] %= 10;
}
return r;
}
ha add(ha b) {
ha r;
for (int i = 0; i < 40; i++) {
r.n[i] += n[i] + b.n[i];
r.n[i + 1] += r.n[i] / 10;
r.n[i] %= 10;
}
return r;
}
ha mul(int x) {
ha r;
for (int i = 0; i < 40; i++) {
r.n[i] = n[i] * x;
}
for (int i = 0; i < 40; i++) {
r.n[i + 1] += r.n[i] / 10;
r.n[i] %= 10;
}
return r;
}
// 默认参数,不要在意这个语法
void print(bool newline = true) {
int i = 40;
while (n[--i] == 0 && i > 0);
while (i >= 0)
putchar(n[i--] + '0');
if (newline) putchar('\n');
}
};
// 重载max对于高精度数的函数
ha max(ha a, ha b) {
for (int i = 40; i >= 0; i--) {
if (a.n[i] > b.n[i]) return a;
else if (a.n[i] < b.n[i]) return b;
}
return a;
}
ha nums[80], result;
ha lineMax(ha * ns, int m) {
// 使用static数组,减少空间创建与释放
static ha dp[80][80];
// 由于使用的是static数组,所以每一次也要清零才能继续
memset(dp, 0, sizeof(dp));
for (int i = 0; i < m; i++) {
dp[i][i] = dp[i][i].add(ns[i]);
// printf("DP[%d][%d] set to: ", i, i); dp[i][i].print();
}
// 思考:为什么要这么写循环?
// 答案可以参考后面的优化
for (int len = 2; len <= m; len++) {
for (int i = 0, j = len - 1; j < m; i++, j++) {
// 这么多printf是用来调试的,请勿在意
// printf("At DP[%d][%d]:\n", i, j);
// printf("\tns[%d]: ", i); ns[i].print(false);
// printf(" ns[%d]: ", j); ns[j].print();
// 分两行写好看一点^_^
dp[i][j] = max(
ns[i].add(dp[i + 1][j].mul(2)),
ns[j].add(dp[i][j - 1].mul(2))
);
// printf("\tDP[%d][%d] set to: ", i, j); dp[i][j].print();
}
}
return dp[0][m - 1].mul(2);
}
int main() {
int n, m;
cin >> n >> m;
ha result;
for (int i = 0; i < n; i++) {
// 每一都要清零
memset(nums, 0, sizeof(nums));
int tmp;
for (int j = 0; j < m; j++) {
cin >> tmp;
nums[j] = nums[j].add(tmp);
}
ha lm = lineMax(nums, m);
// printf("line %d add max: ", i); lm.print();
result = result.add(lm);
}
result.print();
return 0;
}
优化
这里优化一下空间复杂度
如果我们把dp数组的变化给呈现出来,就会发现有一半的空间是没有使用的。
拿样例的第二行1 2 3
举例
最终dp数组应该是这样呈现的:
横是
i
,竖是j
i0 | i1 | i2 | |
---|---|---|---|
j0 | 1 | ||
j1 | 5 | 2 | |
j2 | 17 | 8 | 3 |
最终答案是17 * 2 = 34
我们不难发现,算完(i0, j1),(i0, j0)也就没有什么用了,所以可以压缩一下空间:
其他的代码不变,啊,这就是模块化编程的魅力所在
ha lineMax(ha * ns, int m) {
ha * dp = new ha[m];
for (int i = 0; i < m; i++) {
dp[i] = dp[i].add(ns[i]);
}
for (int len = 1; len < m; len++) {
for (int i = 0; i < m - i; i++)
dp[i] = max(
ns[i].add(dp[i + 1].mul(2)),
ns[i + len].add(dp[i].mul(2))
);
}
return dp[0].mul(2);
}
完事,下课