洛谷 P1005 矩阵取数游戏

[NOIP2007 提高组] 矩阵取数游戏

题目描述

帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的 n × m n \times m n×m 的矩阵,矩阵中的每个元素 a i , j a_{i,j} ai,j 均为非负整数。游戏规则如下:

  1. 每次取数时须从每行各取走一个元素,共 n n n 个。经过 m m m 次后取完矩阵内所有元素;
  2. 每次取走的各个元素只能是该元素所在行的行首或行尾;
  3. 每次取数都有一个得分值,为每行取数的得分之和,每行取数的得分 = 被取走的元素值 × 2 i \times 2^i ×2i,其中 i i i 表示第 i i i 次取数(从 1 1 1 开始编号);
  4. 游戏结束总得分为 m m m 次取数得分之和。

帅帅想请你帮忙写一个程序,对于任意矩阵,可以求出取数后的最大得分。

输入格式

输入文件包括 n + 1 n+1 n+1 行:

第一行为两个用空格隔开的整数 n n n m m m

2 ∼ n + 1 2\sim n+1 2n+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 1n,m30,答案不超过 1 0 16 10^{16} 1016
对于 100 % 100\% 100% 的数据,满足 1 ≤ n , m ≤ 80 1\le n,m\le 80 1n,m80 0 ≤ a i , j ≤ 1000 0\le a_{i,j}\le1000 0ai,j1000

还是想感叹,洛谷这个复制题目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

i0i1i2
j01
j152
j21783

最终答案是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);
}

完事,下课

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值