2020年CSP复赛入门级试题题解
T1 优秀拆分
题目描述
一般来说,一个正整数可以拆分成若干个正整数的和。
例如,, 等。对于正整数 n 的一种特定拆分,我们称它为“优秀的”,当且仅当在这种拆分下,n 被分解为了若干个不同的 2 的正整数次幂。注意,一个数 x 能被表示成 2 的正整数次幂,当且仅当 x 能通过正整数个 2 相乘在一起得到。
例如, 是一个优秀的拆分。但是, 就不是一个优秀的拆分,因为 1 不是 2 的正整数次幂。
现在,给定正整数 n,你需要判断这个数的所有拆分中,是否存在优秀的拆分。若存在,请你给出具体的拆分方案。
输入格式
输入只有一行,一个整数 n,代表需要判断的数。
输出格式
如果这个数的所有拆分中,存在优秀的拆分。那么,你需要从大到小输出这个拆分中的每一个数,相邻两个数之间用一个空格隔开。可以证明,在规定了拆分数字的顺序后,该拆分方案是唯一的。
若不存在优秀的拆分,输出 -1。
输入输出样例
输入 #1
6
输出 #1
4 2
输入 #2
7
输出 #2
-1
说明/提示
样例 1 解释
是一个优秀的拆分。注意, 不是一个优秀的拆分,因为拆分成的 3 个数不满足每个数互不相同。
数据规模与约定
对于 的数据,。
对于另外 的数据,保证 n为奇数。
对于另外 的数据,保证 n 为 2 的正整数次幂。
对于 的数据,。
对于 的数据,。
问题分析
根据题意,“优秀拆分”方案中每个加数均为偶数。因此,奇数不存在优秀拆分。
那么,偶数是否一定具有“优秀拆分”,如何求出拆分方案?。
证明 :每个偶数n显然是可以表示成2的正整数次幂的和。(不要求不相等,可用n / 2 个 2的和来表示)。 如果加数中有两个相同的2的整数次幂(),则这两个加数可合成一个数()。 经过相同数合并,即可产生每个数互不相同的“优秀方案”。因此,偶数一定具有“优秀拆分”。
方法一(模拟合并):先将偶数拆分成 n / 2 个2,进行两两合并,直到个数为一个为止。时间复杂度:,期望得分100分。
#include <bits/stdc++.h> using namespace std; const int dx[3] = {1, -1, 0}; const int dy[3] = {0, 0, 1}; long long a[1010][1010], n, m, vh[1010][1010], ans = -1e10; void dfs(int x, int y, long long sum){ if (x == n && y == m){ //到达目标点 ans = max(ans, sum); return ; } for (int i = 0; i < 3; ++i){ //枚举可能路径 int h = x + dx[i]; int l = y + dy[i]; if (h > 0 && h <= n && l > 0 && l <= m && !vh[h][l]){//可行 vh[h][l] = 1; //修改状态 dfs(h, l, sum + a[h][l]); vh[h][l] = 0; //恢复状态 } } } int main(){ cin >> n >> m; for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) cin >> a[i][j]; vh[1][1] = 1; dfs(1, 1, a[1][1]); cout << ans << endl; return 0; }
方法二(贪心):每次在剩余数的数中拆出一个最大的2的正整数次幂,直到剩余的数变成零。可证明,这样拆分出的加数一定是递减的。时间复杂度:,期望得分100分。
#include <bits/stdc++.h> using namespace std; const int dx[3] = {1, -1, 0}; const int dy[3] = {0, 0, 1}; long long a[1010][1010], n, m, ans = -1e10; void dfs(int x, int y, int dir, long long sum){ if (x == n && y == m){ ans = max(ans, sum); return ; } if (y < m) dfs(x, y + 1, 2, sum + a[x][y + 1]); //向右 if (dir != 0 && x > 1) dfs(x - 1, y, 1, sum + a[x - 1][y]); //向上 if (dir != 1 && x < n) dfs(x + 1, y, 0, sum + a[x + 1][y]); //向下 } int main(){ cin >> n >> m; for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) cin >> a[i][j]; dfs(1, 1, 2, a[1][1]); cout << ans << endl; return 0; }
方法三(二进制拆分):根据进制转换知识,偶数的二进制拆分即对应着“优秀拆分”。时间复杂度:,期望得分100分。
#include <bits/stdc++.h> using namespace std; const int dx[3] = {1, -1, 0}; const int dy[3] = {0, 0, 1}; long long a[1010][1010], n, m, ans = -1e10, dp[1010][1010][3]; void dfs(int x, int y, int dir, long long sum){ if (x == n && y == m){ ans = max(ans, sum); return ; } if (dp[x][y][dir] >= sum) //剪枝 return ; else dp[x][y][dir] = sum; //获得更优解,记录 if (y < m) dfs(x, y + 1, 2, sum + a[x][y + 1]); if (dir != 0 && x > 1) dfs(x - 1, y, 1, sum + a[x - 1][y]); if (dir != 1 && x < n) dfs(x + 1, y, 0, sum + a[x + 1][y]); } int main(){ cin >> n >> m; for (int i = 1; i <= n; ++i) for (int j = 1; j <= m; ++j) cin >> a[i][j]; memset(dp, 128, sizeof(dp)); //初始化为极小值 dfs(1, 1, 2, a[1][1]); cout << ans << endl; return 0; }
T2 直播获奖
题目描述
NOI2130 即将举行。为了增加观赏性,CCF 决定逐一评出每个选手的成绩,并直播即时的获奖分数线。本次竞赛的获奖率为 ,即当前排名前 的选手的最低成绩就是即时的分数线。
更具体地,若当前已评出了 p 个选手的成绩,则当前计划获奖人数为 ,其中 w 是获奖百分比, 表示对 x 向下取整, 表示 x 和 y 中较大的数。如有选手成绩相同,则所有成绩并列的选手都能获奖,因此实际获奖人数可能比计划中多。
作为评测组的技术人员,请你帮 CCF 写一个直播程序。
输入格式
第一行有两个整数 n, w。分别代表选手总数与获奖率。 第二行有 n 整数,依次代表逐一评出的选手成绩。
输出格式
只有一行,包含 n 个非负整数,依次代表选手成绩逐一评出后,即时的获奖分数线。相邻两个整数间用一个空格分隔。
输入输出样例
输入 #1
10 60
200 300 400 500 600 600 0 300 200 100
输出 #1
200 300 400 400 400 500 400 400 300 300
输入 #2
10 30
100 100 600 100 100 100 100 100 100 100
输出 #2
100 100 600 600 600 600 100 100 100 100
说明/提示
样例 1 解释
样例1解释
数据规模与约定
各测试点
对于所有测试点,每个选手的成绩均为不超过 600 的非负整数,获奖百分比 w 是一个正整数且 。
提示
在计算计划获奖人数时,如用浮点类型的变量(如 C/C++ 中的 float 、 double,Pascal 中的 real 、 double 、 extended 等)存储获奖比例 ,则计算 时的结果可能为 3.000001,也可能为 2.999999,向下取整后的结果不确定。因此,建议仅使用整型变量,以计算出准确值。
问题分析
根据题意,测试出 p 个人的成绩,获奖人数为:(整除)。分数线的求法,对成绩从大到小进行排序,第 m 个人的分数即为分数线。
for (int i = 1; i <= n; ++i){
//排序
//计算获奖人数
//输出分数线
}
算法效率的关键是排序算法。
方法一:使用排序算法,如:冒泡、选择等,算法时间复杂度,期望分数:30分。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
int n, w, a[MAXN];
int main(){
cin >> n >> w;
for (int i = 1; i <= n; ++i){
cin >> a[i];
//选择排序
for (int k = 1; k < i; ++k)
for (int j = k + 1; j <= i; ++j)
if (a[k] < a[j]) swap(a[k], a[j]);
int m = max(i * w / 100, 1);
cout << a[m] << " ";
}
return 0;
}
方法二:使用排序算法,如:堆排序,快速排序,stl-sort等,算法时间复杂度,期望分数:50分。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
int n, w, a[MAXN];
int main(){
cin >> n >> w;
for (int i = 1; i <= n; ++i){
cin >> a[i];
//stl-sort排序
sort(a + 1, a + i + 1);
//注意,从小到大排序,解为从后面数的第m个数。
int m = max(i * w / 100, 1);
cout << a[i - m + 1] << " ";
}
return 0;
}
方法三:再思考一波,问题本质数每次加入一个数使原来有序的数列仍然有序。显然,重新排序是没有必要的,只需将这个数插入就可以了。维护有序性效率,算法时间复杂度,期望分数:85分。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
int n, w, a[MAXN];
int main(){
cin >> n >> w;
a[0] = 700;
for (int i = 1; i <= n; ++i){
cin >> a[i];
int t = a[i];
//插入排序
for (int j = i - 1; j >= 0; --j)
if (a[j] < t)
a[j + 1] = a[j];
else{
a[j + 1] = t;
break;
}
int m = max(i * w / 100, 1);
cout << a[m] << " ";
}
return 0;
}
方法四:注意到成绩为不超过600的非负整数,考虑小学生排序,用简单哈希表统计各分数人数。每加一个成绩:1、维护哈希表,效率:. 2、计算分数线需要从600下向统计总人数,当人数大于等于 m 分数就是分数线,效率:。算法时间复杂度,期望分数:100分。
#include <bits/stdc++.h>
using namespace std;
int n, w, vh[610], t;
int main(){
cin >> n >> w;
for (int i = 1; i <= n; ++i){
cin >> t;
vh[t]++;//维护哈希表
int m = max(i * w / 100, 1);
int p = 601, cnt = 0;
//统计并求出分数线
do{
cnt += vh[--p];
} while (cnt < m);
cout << p << " ";
}
return 0;
}
T3 表达式
题目描述
小 C 热衷于学习数理逻辑。有一天,他发现了一种特别的逻辑表达式。在这种逻辑表达式中,所有操作数都是变量,且它们的取值只能为 0 或 1,运算从左往右进行。如果表达式中有括号,则先计算括号内的子表达式的值。特别的,这种表达式有且仅有以下几种运算:
与运算:a & b。当且仅当 a 和 b 的值都为 1 时,该表达式的值为 1。其余情况该表达式的值为 0。
或运算:a | b。当且仅当 a 和 b 的值都为 0 时,该表达式的值为 0。其余情况该表达式的值为 1。
取反运算:!a。当且仅当 a 的值为 0 时,该表达式的值为 1。其余情况该表达式的值为 0。
小 C 想知道,给定一个逻辑表达式和其中每一个操作数的初始取值后,再取反某一个操作数的值时,原表达式的值为多少。
为了化简对表达式的处理,我们有如下约定:
表达式将采用后缀表达式的方式输入。
后缀表达式的定义如下:
如果 E 是一个操作数,则 E 的后缀表达式是它本身。
如果 E 是 形式的表达式,其中 是任何二元操作符,且优先级不高于 E1、 E2 中括号外的操作符,则 E 的后缀式为 ,其中 分别为 E1、E2 的后缀式。
如果 E 是 E1 形式的表达式,则 E1 的后缀式就是 E 的后缀式。
同时为了方便,输入中:
与运算符(&)、或运算符(|)、取反运算符(!)的左右均有一个空格,但表达式末尾没有空格。
操作数由小写字母 x 与一个正整数拼接而成,正整数表示这个变量的下标。例如:x10,表示下标为 10 的变量 。数据保证每个变量在表达式中出现恰好一次。
输入格式
第一行包含一个字符串 s,表示上文描述的表达式。 第二行包含一个正整数 n,表示表达式中变量的数量。表达式中变量的下标为 。 第三行包含 n 个整数,第 i 个整数表示变量 的初值。 第四行包含一个正整数 q,表示询问的个数。 接下来 q 行,每行一个正整数,表示需要取反的变量的下标。注意,每一个询问的修改都是临时的,即之前询问中的修改不会对后续的询问造成影响。 数据保证输入的表达式合法。变量的初值为 0 或 1。
输出格式
输出一共有 q 行,每行一个 0 或 1,表示该询问下表达式的值。
输入输出样例
输入 #1
x1 x2 & x3 |
3
1 0 1
3
1
2
3
输出 #1
1
1
0
输入 #2
x1 ! x2 x4 | x3 x5 ! & & ! &
5
0 1 0 1 1
3
1
3
5
输出 #2
0
1
1
说明/提示
样例 1 解释
该后缀表达式的中缀表达式形式为 。
对于第一次询问,将 的值取反。此时,三个操作数对应的赋值依次为 0,0,1。原表达式的值为 (0\&0)|1=1。
对于第二次询问,将 的值取反。此时,三个操作数对应的赋值依次为 1,1,1。原表达式的值为 (1\&1)|1=1。
对于第三次询问,将 的值取反。此时,三个操作数对应的赋值依次为 1,0,0。原表达式的值为 (1\&0)|0=0。
样例 2 解释
该表达式的中缀表达式形式为 。
数据规模与约定
对于 的数据,表达式中有且仅有与运算(&)或者或运算(|)。
对于另外 的数据,。
对于另外 的数据,变量的初值全为 0 或全为 1。
对于 的数据,。
其中,|s| 表示字符串 s 的长度。
问题分析
首先,根据确定的 x 值,可计算出后缀表达式的值。
方法是:
读到变量,则入栈;
读到“!”,从栈中弹出一个数,取反后再入栈。
读到“&”或“|”,从栈中弹出两个数, 做"&&"或“||”运算,结果再入栈。
最后栈内的值就是结果。
方法一:每次修改一个x值,用上面的方法计算结果。时间复杂度为:,期望得分:30分。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
string st;
bool xv[MAXN];
int n, q, id;
void Readp(){
getline(cin, st);
st += " ";//最后加一个空格,方便处理
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> xv[i];
}
//计算后缀表达式的值
bool work(){
stack <bool> stk;
int len = st.size(), v = -1;//v用来存储变量标号
bool op1, op2;
for (int i = 0; i < len; ++i){
if (st[i] == 'x'){
v = 0;
} else if (st[i] >= '0' && st[i] <= '9'){
v = v * 10 + st[i] - '0';
} else if (st[i] == ' '){
if (v != -1){ //标号计算结束,入栈
stk.push(xv[v] == 1);
v = -1;
}
} else if (st[i] == '!'){
op1 = stk.top();
stk.pop();
stk.push(!op1);
} else if (st[i] == '|'){
op1 = stk.top();
stk.pop();
op2 = stk.top();
stk.pop();
stk.push(op1 || op2);
} else if (st[i] == '&'){
op1 = stk.top();
stk.pop();
op2 = stk.top();
stk.pop();
stk.push(op1 && op2);
}
}
return stk.top();
}
int main(){
Readp();
cin >> q;
for (int i = 1; i <= q; ++i){
cin >> id;
xv[id] = !xv[id]; //每修改一个变量
cout << work() << endl; //计算表达式的值并输出
xv[id] = !xv[id]; //变量还原
}
return 0;
}
方法二:观察“数据规模与约定”,的数据只有&或|运算。这样的逻辑式就是n个bool型的逻辑与(或)运算,只要数一个false(true)的个数就可以了。对数据进行特判处理,加上方法一,期望得分:50分。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
string st;
bool xv[MAXN];
int n, q, id;
void Readp(){
getline(cin, st);
st += " ";
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> xv[i];
}
char check(){
bool f1, f2, f3;
f1 = f2 = f3 = false;
int len = st.size();
for (int i = 0; i < len; ++i)
if (st[i] == '&')
f1 = true;
else if (st[i] == '|')
f2 = true;
else if (st[i] == '!')
f3 = true;
if (f1 == true && f2 == false && f3 == false)
return '&';
else if (f1 == false && f2 == true && f3 == false)
return '|';
else
return '#';
}
bool cal(){
stack <bool> stk;
int len = st.size(), v = -1;
bool op1, op2;
for (int i = 0; i < len; ++i){
if (st[i] == 'x'){
v = 0;
} else if (st[i] >= '0' && st[i] <= '9'){
v = v * 10 + st[i] - '0';
} else if (st[i] == ' '){
if (v != -1){
stk.push(xv[v] == 1);
v = -1;
}
} else if (st[i] == '!'){
op1 = stk.top();
stk.pop();
stk.push(!op1);
} else if (st[i] == '|'){
op1 = stk.top();
stk.pop();
op2 = stk.top();
stk.pop();
stk.push(op1 || op2);
} else if (st[i] == '&'){
op1 = stk.top();
stk.pop();
op2 = stk.top();
stk.pop();
stk.push(op1 && op2);
}
}
return stk.top();
}
void work(){
cin >> q;
for (int i = 1; i <= q; ++i){
cin >> id;
//cout << id << endl;
xv[id] = !xv[id];
cout << cal() << endl;
xv[id] = !xv[id];
}
}
void work1(){
int s = 0;
for (int i = 1; i <= n; ++i)
if (xv[i] == 0) s++;
cin >> q;
for (int i = 1; i <= q; ++i){
cin >> id;
int s1 = s;
if (xv[id] == 0) s--;
else s++;
if (s == 0)
cout << 1 << endl;
else
cout << 0 << endl;
s = s1;
}
}
void work2(){
int s = 0;
for (int i = 1; i <= n; ++i)
if (xv[i] == 1) s++;
cin >> q;
for (int i = 1; i <= q; ++i){
cin >> id;
int s1 = s;
if (xv[id] == 1) s--;
else s++;
if (s == 0)
cout << 0 << endl;
else
cout << 1 << endl;
s = s1;
}
}
int main(){
Readp();
char ch = check();
if (ch == '&')
work1();
else if (ch == '|')
work2();
else
work();
return 0;
}
方法三:建立表达式树。注意到每次只修改一个变量值,flag[x]表示 x 节点数值反转是否会导致表达式结果反转。
1、根节点结反转变化会导致表达式结果反转 。
2、若某个节点x的值反转可以改变表达式的值,其子节点的情况分析如下:
若该节点的运算符为 ! ,则其子节点值反转同样可以使表达式的值反转。
若该节点的运算符为 | , 如果该节点的值为false,则子节点反转会引起该节点反转,也会引起表达式值反转。如果该节点的值为true,则可分成两种情况:1、两个子节点的值分别均为ture,则子节点反转不会引起该节点反转,即表达式的值也不会反转。2、两个子节点分别为false和true,值为true的子节点反转,会引起该节点和表达式值的反转。
若该节点的运算符为 &, 如果该节点的值为true,则子节点反转会引起该节点反转,也会引起表达式值反转。如果该节点的值为false,则可分成两种情况:1、两个子节点的值分别均为false,则子节点反转不会引起该节点反转,即表达式的值也不会反转。2、两个子节点分别为false和true,值为true的子节点反转,会引起该节点和表达式值的反转。
将flag节点初始化为false,从根节点开始dfs,搜索所有值反转导致表达式结果反转的节点并标记。
时间复杂度:,期望得分:100分。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100010;
//l,r 分别表示左右节点编号, v用来存储该节点的值, op表示操作符。
struct nod{
int l, r;
bool v;
char op;
} tree[MAXN * 4];
string st;
bool flag[MAXN * 4];
int n, q, id, t, cnt;
void Readp(){
getline(cin, st);
st += " ";
cin >> n;
cnt = n;
for (int i = 1; i <= n; ++i){
cin >> tree[i].v;
tree[i].l = tree[i].r = 0;
tree[i].op = '#';//表示该节点是变量
}
}
void Build(){ //建树
stack <int> stk;
int len = st.size(), v = -1, op1, op2;
for (int i = 0; i < len; ++i){
if (st[i] == 'x'){
v = 0;
} else if (st[i] >= '0' && st[i] <= '9'){
v = v * 10 + st[i] - '0';
} else if (st[i] == ' '){
if (v != -1){
stk.push(v);
v = -1;
}
} else if (st[i] == '!'){
op1 = stk.top();
stk.pop();
cnt++;
tree[cnt].l = op1;
tree[cnt].r = 0;
tree[cnt].v = !tree[op1].v;
tree[cnt].op = '!';
stk.push(cnt);
} else if (st[i] == '|'){
op1 = stk.top();
stk.pop();
op2 = stk.top();
stk.pop();
cnt++;
tree[cnt].l = op1;
tree[cnt].r = op2;
tree[cnt].v = tree[op1].v || tree[op2].v;
tree[cnt].op = '|';
stk.push(cnt);
} else if (st[i] == '&'){
op1 = stk.top();
stk.pop();
op2 = stk.top();
stk.pop();
cnt++;
tree[cnt].l = op1;
tree[cnt].r = op2;
tree[cnt].v = tree[op1].v && tree[op2].v;
tree[cnt].op = '&';
stk.push(cnt);
}
}
}
void Dfs(int root){
flag[root] = 1; //标记该节点,反转会导致表达式结果反转
if (tree[root].op == '#') //叶节点,是变量
return;
if (tree[root].op == '!')
Dfs(tree[root].l);
if (tree[root].op == '|'){
if (tree[root].v == true){
if (tree[tree[root].l].v == false)
Dfs(tree[root].r);
if (tree[tree[root].r].v == false)
Dfs(tree[root].l);
} else {
Dfs(tree[root].l);
Dfs(tree[root].r);
}
}
if (tree[root].op == '&'){
if (tree[root].v == false){
if (tree[tree[root].l].v == true)
Dfs(tree[root].r);
if (tree[tree[root].r].v == true)
Dfs(tree[root].l);
} else {
Dfs(tree[root].l);
Dfs(tree[root].r);
}
}
}
int main(){
Readp();
Build();
memset(flag, 0, sizeof(flag)); //将flag数组初始化为0
Dfs(cnt);
cin >> q;
for (int i = 1; i <= q; ++i){
cin >> id;
if (flag[id])
cout << !tree[cnt].v << endl;
else
cout << tree[cnt].v << endl;
}
return 0;
}
T4 方格取数
题目描述
设有 的方格图,每个方格中都有一个整数。现有一只小熊,想从图的左上角走到右下角,每一步只能向上、向下或向右走一格,并且不能重复经过已经走过的方格,也不能走出边界。小熊会取走所有经过的方格中的整数,求它能取到的整数之和的最大值。
输入格式
第一行有两个整数 n, m。
接下来 n 行每行 m 个整数,依次代表每个方格中的整数。
输出格式
一个整数,表示小熊能取到的整数之和的最大值。
输入输出样例
输入 #1
3 4
1 -1 3 2
2 -1 4 -1
-2 2 -3 -1
输出 #1
9
输入 #2
2 5
-1 -1 -3 -2 -7
-2 -1 -4 -1 -2
输出 #2
-10
说明/提示
样例1说明
样例2说明
数据规模与约定
对于 的数据,。
对于 的数据,。
对于 的数据,。
对于 的数据,。方格中整数的绝对值不超过。
问题分析
方法一(爆搜):每次尝试三个方向,如果可以走(用vh数组标记哪些点已经走过),记录当前取得的数的和;到达目标点,打擂台求最大值。
时间复杂度:,期望得分20分。
#include <bits/stdc++.h>
using namespace std;
const int dx[3] = {1, -1, 0};
const int dy[3] = {0, 0, 1};
long long a[1010][1010], n, m, vh[1010][1010], ans = -1e10;
void dfs(int x, int y, long long sum){
if (x == n && y == m){ //到达目标点
ans = max(ans, sum);
return ;
}
for (int i = 0; i < 3; ++i){ //枚举可能路径
int h = x + dx[i];
int l = y + dy[i];
if (h > 0 && h <= n && l > 0 && l <= m && !vh[h][l]){//可行
vh[h][l] = 1; //修改状态
dfs(h, l, sum + a[h][l]);
vh[h][l] = 0; //恢复状态
}
}
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> a[i][j];
vh[1][1] = 1;
dfs(1, 1, a[1][1]);
cout << ans << endl;
return 0;
}
方法二(爆搜):对状态进行优化,只能向右,向上, 向下走,出现重复路径只能是“上次向上,这次下向”或“上次向下,这次向上”,因此,每次可选择的方向只与上次的方向有关。加一个参数记录每次行走的方向,就可以避免走重复路径,不需要使用vh数组。
意义:当前点的状态只用三个参数(x, y, dir)表达,而方法一的状态与vh数组有关;可以开表记录每个点的最优解,使得最优剪枝,记忆化搜索成为可能。
时间复杂度:,期望得分:20分。
#include <bits/stdc++.h>
using namespace std;
const int dx[3] = {1, -1, 0};
const int dy[3] = {0, 0, 1};
long long a[1010][1010], n, m, ans = -1e10;
void dfs(int x, int y, int dir, long long sum){
if (x == n && y == m){
ans = max(ans, sum);
return ;
}
if (y < m) dfs(x, y + 1, 2, sum + a[x][y + 1]); //向右
if (dir != 0 && x > 1)
dfs(x - 1, y, 1, sum + a[x - 1][y]); //向上
if (dir != 1 && x < n)
dfs(x + 1, y, 0, sum + a[x + 1][y]); //向下
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> a[i][j];
dfs(1, 1, 2, a[1][1]);
cout << ans << endl;
return 0;
}
方法三(最优化剪枝):显然,本题具有最优解子结构的特征,即最优解路径方案中,起点到达中间的任意点取得的数和都是起点到该点的最优解。
若到达某个状态,取得数和s,进行深搜;下一次再到该状态并且取的数和小于等于s时,是不会搜索到更优解的。因此,这种状态就可以剪枝了。
开个dp数组来记录每个状态的当前最优解,如果在某个状态下的当前数和不小于前面获得的最优解,则进行剪枝。
时间效率不好估算,如果数据随机的话,效率还是不错的。实测得分:40分。
#include <bits/stdc++.h>
using namespace std;
const int dx[3] = {1, -1, 0};
const int dy[3] = {0, 0, 1};
long long a[1010][1010], n, m, ans = -1e10, dp[1010][1010][3];
void dfs(int x, int y, int dir, long long sum){
if (x == n && y == m){
ans = max(ans, sum);
return ;
}
if (dp[x][y][dir] >= sum) //剪枝
return ;
else
dp[x][y][dir] = sum; //获得更优解,记录
if (y < m) dfs(x, y + 1, 2, sum + a[x][y + 1]);
if (dir != 0 && x > 1)
dfs(x - 1, y, 1, sum + a[x - 1][y]);
if (dir != 1 && x < n)
dfs(x + 1, y, 0, sum + a[x + 1][y]);
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> a[i][j];
memset(dp, 128, sizeof(dp)); //初始化为极小值
dfs(1, 1, 2, a[1][1]);
cout << ans << endl;
return 0;
}
方法四(递归 + 记忆化):定义函数dfs(x, y, dir)表示(x, y, dir)状态走到(n, m)能取得的最大数。可写递归函数:
递归关系
递归实现 + 记忆化搜索。
时间复杂度,期望得分:100分。
#include <bits/stdc++.h>
using namespace std;
const int dx[3] = {1, -1, 0};
const int dy[3] = {0, 0, 1};
long long a[1010][1010], n, m, ans = -1e10, dp[1010][1010][3];
long long dfs(int x, int y, int dir){
if (dp[x][y][dir] != -1) //如果该节点已经计算过,直接返回值。
return dp[x][y][dir];
if (x == n && y == m){ //边界
return dp[x][y][dir] = a[n][m];
}
long long maxv = -1e10;
if (y < m) maxv = max(maxv, dfs(x, y + 1, 2));
if (dir != 0 && x > 1)
maxv = max(maxv, dfs(x - 1, y, 1));
if (dir != 1 && x < n)
maxv = max(maxv, dfs(x + 1, y, 0));
return dp[x][y][dir] = maxv + a[x][y];
}
int main(){
cin >> n >> m;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
cin >> a[i][j];
memset(dp, -1, sizeof(dp));
cout << dfs(1, 1, 2) << endl;
return 0;
}
https://www.bilibili.com/read/cv13171191
作者:newcode 更多资源请关注纽扣编程微信公众号
从事机器人比赛、机器人等级考试、少儿scratch编程、信息学奥赛等研究学习