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