SJTU OJ¶
AC题目编号及解答:SJTU_OJ source
1002.二哥种花生¶
题目地址 1002
二哥在自己的后花园里种了一些花生,也快到了收获的时候了。这片花生地是一个长度为L、宽度为W的矩形,每个单位面积上花生产量都是独立的。他想知道,对于某个指定的区域大小,在这么大的矩形区域内,花生的产量最大会是多少。
这道题理解题意之后就是一道典型的二维前缀和相关的题目。有关前缀和的相关题目与介绍可以参见:
介绍的非常详细,并包括很多开拓的思维。
题目本身很好理解,在一个二维数组中,给定一个大小,求解在该大小下数组中最大的那一块区域。 老样子,如果是使用暴力求解,时间复杂度应该会到达 \(O(n^4)\) ,两个外层循环遍历出所有可能 的起始位置(也就是框定大小的左上角坐标),两个内层循环计算出该框定区域的和,最后比较大小得出结果。 暴力解最后还是超时了。(虽然知道要超时但是还是会提交一下xD)
解决办法就是在读入数据的时候构建该二维数组的二维前缀和,然后依然是两个外层循环遍历所有的起始位置, 不过在计算每个具体的框定大小的和时只需要 \(O(1)\) 时间,最后的时间复杂度能降到 \(O(n^2)\) 。
#include<iostream>
#include<memory.h>
using namespace std;
#define MAX 1001
int arr[MAX][MAX];
int map[MAX][MAX];
int main() {
int l, w;
cin >> l >> w;
for (int i = 1; i <= l; i++) {
for (int j = 1; j <= w; j++) {
cin >> arr[i][j];
}
}
int a, b;
cin >> a >> b;
int m = 0;
memset(map, 0, sizeof(map));
for (int i = 1; i <= l; i++) {
for (int j = 1; j <= w; j++) {
map[i][j] = map[i - 1][j] + map[i][j - 1] - map[i - 1][j - 1] + arr[i][j];
if (i >= a && j >= b) {
m = m > map[i][j] - map[i - a][j] - map[i][j - b] + map[i - a][j - b] ? m : map[i][j] - map[i - a][j] - map[i][j - b] + map[i - a][j - b];
}
}
}
//int temp;
//for (int p = 0; p < l - a + 1; p++) {
// for (int q = 0; q < w - b + 1; q++) {
// temp = 0;
// for (int x = 0; x < a; x++) {
// for (int y = 0; y < b; y++) {
// temp += arr[p + x][q + y];
// }
// }
// m = temp > m ? temp : m;
// }
//}
cout << m;
return 0;
}
裂墙推荐去看上面的链接。
1006.求和游戏¶
题目地址 1006
石柱上有一排石头键盘,每个键上有一个整数。请你在键盘上选择两个键,使这两个键及其之间的键上的数字和最大。如果这个最大的和不为正,则输出“Game Over”。
这道题理解后可变形为求解一数组中 最大的连续子数组的和 ,其中子数组的 长度必须大于一 。 要求长度大于一的原因在于题目里提到了需要选择两个键。
如果没有后面这个子数组长度必须大于一的条件,这个问题是一个经典的动态规划问题,能够在 \(O(n)\) 时间内求解。
int solve(int *a, int len) {
int ans = -INF, dp = 0;
for(int i = 0; i < len; i++){
if(dp > 0) {
dp += a[i];
}
else{
dp = a[i];
}
if(dp > ans){
ans = dp;
}
}
return ans;
}
这段代码对我影响很大,当时算法课上老师请了一位校队同学给我们讲解动态规划。 [1] 那位同学的ppt中第一 道题目就是这个,那是我第一次看到这么优美的解法。
回到题目,最直接的解法可以暴力,通过三个循环来实现。首先通过两个循环遍历 得到所有可能的两个键,最后通过一个循环来求的两个键之间的数组元素和。总复杂度达到 \(O(n^3)\) 。 当然这个暴力的解法也可以降到 \(O(n^2)\) 。在输入元素的时候构建该数组的前缀和数组,则此时在 最后一次循环中求解两个键之间的元素和的操作可以在 \(O(1)\) 的时间内完成。
假设在读入时已经构建了该数组的前缀和数组(前缀和数组从下标1开始计数,下标0的位置设定值为0,这样是 为了写递推式的时候方便一些),则:
int solve(int *a, int len){ // 这里传前缀和数组进来
int ans = -INF, t = 0;
for(int i = 1; i < len; i++){
for(int j = i + 1; j <= len; j++){
// 计算下标为i到j(包含两端点)的子数组的和,可以在O(1)时间完成
t = a[j] - a[i - 1];
ans = t > ans ? t : ans;
t = 0;
}
}
}
提交结果最后还是超时了,看来需要在 \(O(n)\) 的方法上改进一下以满足题目的要求。
2020.7.21最后还是A了这道题,也没去找别人的思路,直接在 \(O(n)\) 的方法上改进了:
int solve(int *a, int len){
int ans = -INF, dp = 0, predp = 0;
for (int i = 0; i < len; i++) {
predp = dp;
if (dp > 0) {
dp += a[i];
ans = dp > ans ? dp : ans;
}
else {
dp = a[i];
if (dp > 0) {
ans = (dp + predp) > ans ? (dp + predp) : ans;
}
}
}
}
这一题的思路现在还是没法很好的系统的描述出来(但是代码能写出来),等过段时间看多了dp之后再回头 来看。
改进的主要思路是处理了可能出现 负 + 正 = 正 并做为结果的情况。例如输入[3, -6, 7]或者是 [-2, -1, 9]这样的情况。
—————