CSP-J 2020題解
CSP-J 2020題解
本次考試還是很有用的,至少把我澆了一盆冷水。
當使用民間資料自測的時候,我就自閉了。
估分是320,但有些比較低階的錯誤直接少掉100。
而且這套題應該上350才正常吧,也不是像平時訓練一樣難。
主要是平時的時候太依賴於評測機了,小錯誤就都沒注意,我認為在平時訓練當中就應該一次成型,因為考試只有一次提交機會。
基礎概念也需要搞清楚,比如運算優先順序等等。
心態也要調整好。就這次考試而言,我的心態是極差的。多在考試之前鼓勵下自己。可能就會好些吧!
調整心態,繼續努力,把自己當做起跑線上的人,路還很長,我們還會繼續!!
不多說了,上題解
T1 優秀的拆分
題目描述
一般來說,一個正整數可以拆分成若干個正整數的和。
例如,\(1=1\),\(10=1+2+3+4\) 等。對於正整數\(n\)的一種特定拆分,我們稱它為“優秀的”,當且僅當在這種拆分下,\(n\)被分解為了若干個不同的\(2\)的正整數次冪。注意,一個數\(x\)能被表示成\(2\)的正整數次冪,當且僅當\(x\)能通過正整數個\(2\)相乘在一起得到。
例如,\(10=8+2=2^3+2^1\)是一個優秀的拆分。但是,\(7=4+2+1=2^2+2^1+2^0\)就不是一個優秀的拆分,因為\(1\)不是\(2\)的正整數次冪。
現在,給定正整數\(n\),你需要判斷這個數的所有拆分中,是否存在優秀的拆分。若存在,請你給出具體的拆分方案。
輸入格式
輸入只有一行,一個整數\(n\),代表需要判斷的數。
輸出格式
如果這個數的所有拆分中,存在優秀的拆分。那麼,你需要從大到小輸出這個拆分中的每一個數,相鄰兩個數之間用一個空格隔開。可以證明,在規定了拆分數字的順序後,該拆分方案是唯一的。
若不存在優秀的拆分,輸出 -1。
輸入輸出樣例
輸入 #1
6
輸出 #1
4 2
輸入 #2
7
輸出 #2
-1
說明/提示
樣例 1 解釋
\(6=4+2=2^2+2^1\)是一個優秀的拆分。注意,\(6=2+2+2\)不是一個優秀的拆分,因為拆分成的\(3\)個數不滿足每個數互不相同。
資料規模與約定
對於\(20\%\)的資料,\(n \le 10\)
對於另外\(20\%\)的資料,保證\(n\)為奇數。
對於另外\(20%\)的資料,保證\(n\)為\(2\)的正整數次冪。
對於\(80\%\)的資料,\(n \le 1024\)。
對於\(100\%\)的資料,\(1 \le n \le 1 \times 10^7\)。
思路
簡單,倒著列舉二的正整數次冪即可。
C++程式碼
#include <queue>
#include <cstdio>
using namespace std;
queue<int> q;
int n;
int main() {
scanf("%d", &n);
for(int i = 30; i >= 1; i--) {
int now = (1 << i);
if(now <= n) {
n -= now;
q.push(now);
}
}
if(n != 0)
printf("-1");
else
while(!q.empty()) {
printf("%d ", q.front());
q.pop();
}
return 0;
}
T2 直播獲獎
題目描述
NOI2130 即將舉行。為了增加觀賞性,CCF 決定逐一評出每個選手的成績,並直播即時的獲獎分數線。本次競賽的獲獎率為\(w\%\),即當前排名前\(w\%\)的選手的最低成績就是即時的分數線。
更具體地,若當前已評出了\(p\)個選手的成績,則當前計劃獲獎人數為\(\max(1, \lfloor p * w \%\rfloor)\),其中\(w\)是獲獎百分比,\(\lfloor x \rfloor\)表示對\(x\)向下取整,\(\max(x,y)\)表示\(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 解釋
資料規模與約定
對於所有測試點,每個選手的成績均為不超過\(600\)的非負整數,獲獎百分比\(w\)是一個正整數且\(1 \le w \le 99\)。
提示
在計算計劃獲獎人數時,如用浮點型別的變數(如 C/C++ 中的 float 、 double,Pascal 中的 real 、 double 、 extended 等)儲存獲獎比例 w%w%,則計算\(5 \times 60\%\)時的結果可能為\(3.000001\),也可能為\(2.999999\),向下取整後的結果不確定。因此,建議僅使用整型變數,以計算出準確值。
思路
使用一個大根堆與一個小根堆來模擬。
大根堆來儲存分數線以下的分數。
小根堆來儲存分數線以上的分數。
模擬分數線的波動即可。
C++程式碼
#include <queue>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 1e5 + 5;
priority_queue<int, vector<int>, greater<int> > q1;
priority_queue<int> q2;
int a[MAXN];
int n, w;
int main() {
scanf("%d %d", &n, &w);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
int id = 1;
q1.push(a[1]);
printf("%d ", a[1]);
for(int i = 2; i <= n; i++) {
int now = i * w / 100;
if(a[i] > q1.top()) {
q1.push(a[i]);
int k = q1.top();
q2.push(k);
q1.pop();
}
else {
q2.push(a[i]);
}
if(now > id) {
id = now;
int k = q2.top(); q2.pop();
q1.push(k);
}
printf("%d ", q1.top());
}
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\)是\(E_1~\texttt{op}~E\)形式的表示式,其中\(\texttt{op}\)是任何二元操作符,且優先順序不高於\(E_1\)、\(E_2\)中括號外的操作符,則\(E\)的字尾式為 \(E_1' E_2' \texttt{op}\),其中\(E_1'\)、\(E_2'\)分別為\(E_1\)、\(E_2\)的字尾式。
- 如果\(E\)是\(E_1\)形式的表示式,則\(E_1\)的字尾式就是\(E\)的字尾式。
同時為了方便,輸入中:
與運算子(&)、或運算子(|)、取反運算子(!)的左右均有一個空格,但表示式末尾沒有空格。
運算元由小寫字母\(x\)與一個正整數拼接而成,正整數表示這個變數的下標。例如:x10,表示下標為\(10\)的變數\(x_{10}\)。資料保證每個變數在表示式中出現恰好一次。
輸入格式
第一行包含一個字串\(s\),表示上文描述的表示式。
第二行包含一個正整數\(n\),表示表示式中變數的數量。表示式中變數的下標為\(1,2, \cdots , n\)。
第三行包含\(n\)個整數,第\(i\)個整數表示變數\(x_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 解釋
該字尾表示式的中綴表示式形式為\((x_1 \& x_2) | x_3\)。
對於第一次詢問,將\(x_1\)的值取反。此時,三個運算元對應的賦值依次為\(0\),\(0\),\(1\)。原表示式的值為\((0\&0)|1=1\)。
對於第二次詢問,將\(x_2\)的值取反。此時,三個運算元對應的賦值依次為\(1\),\(1\),\(1\)。原表示式的值為\((1\&1)|1=1\)。
對於第三次詢問,將\(x_3\)的值取反。此時,三個運算元對應的賦值依次為\(1\),\(0\),\(0\)。原表示式的值為\((1\&0)|0=0\)。
樣例 2 解釋
該表示式的中綴表示式形式為\((!x_1)\&(!((x_2|x_4)\&(x_3\&(!x_5))))\)。
資料規模與約定
對於\(20\%\)的資料,表示式中有且僅有與運算(&)或者或運算(|)。
對於另外\(30\%\)的資料,\(|s| \le 1000\),\(q \le 1000\),\(n \le 1000\)。
對於另外\(20\%\)的資料,變數的初值全為\(0\)或全為\(1\)。
對於\(100\%\)的資料,\(1 \le |s| \le 1 \times 10^6\),\(1 \le q \le 1 \times 10^5\),\(2 \le n \le 1 \times 10^5\)。
其中,\(|s|\)表示字串\(s\)的長度。
思路
首先對於僅有或運算和且運算的情況,不難想到可以騙分,再利用字尾表示式跑一遍暴力就可以騙到50分,時間複雜度為\(O(nq)\)。
這種做法可以優化嗎?或運算和且運算都有兩個數字進行運算,是不是很像一顆二叉樹。而且這顆二叉樹有一個特性:每個父節點都必有兩個子節點。利用這一點,可以去看若改變這顆樹上的某一個值,會不會對本次計算產生影響,進而推出是否會對整個結果產生影響,預處理一次時間複雜度為\(O(n)\),即便利這整棵樹。總的來看加上後面的查詢,時間複雜度為\(O(n+q)\)。
C++程式碼
#include <stack>
#include <cstdio>
#include <string>
#include <iostream>
using namespace std;
void Quick_Read(int &Number) {
Number = 0;
char c = getchar();
int op = 1;
while (c < '0' || c > '9') {
if (c == '-')
op = -1;
c = getchar();
}
while (c >= '0' && c <= '9') {
Number = (Number << 1) + (Number << 3) + c - 48;
c = getchar();
}
Number *= op;
}
const int MAXN = 1e6 + 5;
struct Node {
int Left_Child, Right_Child;
int Value, Index, Operator;
};
stack<int> s;
string c;
Node Tree[MAXN];
bool Alter[MAXN];
int Num[MAXN];
int len, n, q;
void calc(int x) {
if(Tree[x].Operator == -1) {
Alter[Tree[x].Index] = 1;
return;
}
if(!Tree[x].Operator)
calc(Tree[x].Left_Child);
else if(Tree[x].Operator == 1) {
int Left_Child = Tree[x].Left_Child;
int Right_Child = Tree[x].Right_Child;
if(Tree[Left_Child].Value + Tree[Right_Child].Value == 2) {
calc(Left_Child);
calc(Right_Child);
}
else if(Tree[Left_Child].Value)
calc(Right_Child);
else if(Tree[Right_Child].Value)
calc(Left_Child);
}
else {
int Left_Child = Tree[x].Left_Child;
int Right_Child = Tree[x].Right_Child;
if(Tree[Left_Child].Value + Tree[Right_Child].Value == 0) {
calc(Left_Child);
calc(Right_Child);
}
else if(!Tree[Left_Child].Value)
calc(Right_Child);
else if(!Tree[Right_Child].Value)
calc(Left_Child);
}
}
void Read() {
int A;
getline(cin, c);
Quick_Read(n);
for(int i = 1; i <= n; i++)
Quick_Read(Num[i]);
for(int i = 0; i < c.length(); i++) {
if(c[i] == 'x') {
int x = 0;
while(c[i + 1] <= '9' && c[i + 1] >= '0') {
x = x * 10 + c[i + 1] - 48;
i++;
}
len++;
Tree[len].Value = Num[x];
Tree[len].Index = x;
Tree[len].Operator = -1;
s.push(len);
continue;
}
if(c[i] == '!') {
len++;
Tree[len].Value = !Tree[s.top()].Value;
Tree[len].Left_Child = s.top(); s.pop();
Tree[len].Operator = 0;
s.push(len);
continue;
}
if(c[i] == '&') {
len++;
int Number1 = s.top(); s.pop();
int Number2 = s.top(); s.pop();
Tree[len].Value = Tree[Number1].Value & Tree[Number2].Value;
Tree[len].Operator = 1;
Tree[len].Left_Child = Number1;
Tree[len].Right_Child = Number2;
s.push(len);
continue;
}
if(c[i] == '|') {
len++;
int Number1 = s.top(); s.pop();
int Number2 = s.top(); s.pop();
Tree[len].Value = Tree[Number1].Value | Tree[Number2].Value;
Tree[len].Operator = 2;
Tree[len].Left_Child = Number1;
Tree[len].Right_Child = Number2;
s.push(len);
continue;
}
}
}
void Write() {
int A;
Quick_Read(q);
for(int i = 1; i <= q; i++) {
Quick_Read(A);
if(Alter[A])
printf("%d\n", !Tree[len].Value);
else
printf("%d\n", Tree[len].Value);
}
}
int main() {
Read();
calc(len);
Write();
return 0;
}
T4 方格取數
題目描述
設有\(n \times m\)的方格圖,每個方格中都有一個整數。現有一隻小熊,想從圖的左上角走到右下角,每一步只能向上、向下或向右走一格,並且不能重複經過已經走過的方格,也不能走出邊界。小熊會取走所有經過的方格中的整數,求它能取到的整數之和的最大值。
輸入格式
第一行有兩個整數\(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 + (-1) + 4 + 3 + 2 + (-1) + (-1) = 9\),可以證明為最大值。
注意,上述走法是錯誤的,因為第\(2\)行第\(2\)列的方格走過了兩次,而根據題意,不能重複經過已經走過的方格。
另外,上述走法也是錯誤的,因為沒有走到右下角的終點。
資料規模與約定
對於\(20\%\)的資料,\(n, m \le 5\)。
對於\(40\%\)的資料,\(n, m \le 50\)。
對於\(70\%\)的資料,\(n, m \le 300\)。
對於\(100\%\)的資料,\(1 \le n,m \le 10^3\)。方格中整數的絕對值不超過\(10^4\)。
思路
看到這道題的時候想到的是用最短路。
但是仔細想想,若用Dijkstra有正邊有負邊,不可以用。若用SPFA,回有環。若強行用分層圖來維護的話,時間複雜度就為\(O(n^2mlog(n^2m))\),肯定會超時。
於是在考場上就想了一個70分的dp帶字首和優化,時間複雜度為\(O(n^2m)\) 。
程式碼如下:
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 1e3 + 5;
int dp[MAXN][MAXN];
int dist[MAXN];
int a[MAXN][MAXN];
int n, m;
int main() {
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= i; j++)
dp[1][i] += a[j][1];
}
for(int i = 2; i <= m; i++) {
dist[1] = a[1][i];
for(int j = 2; j <= n; j++)
dist[j] = dist[j - 1] + a[j][i];
for(int j = 1; j <= n; j++)
dp[i][j] = dp[i - 1][j] + a[j][i];
for(int j = 1; j <= n; j++) {
for(int k = 1; k <= n; k++) {
dp[i][j] = max(dp[i][j], dp[i - 1][k] + dist[max(j, k)] - dist[min(j, k) - 1]);
}
}
}
printf("%d", dp[m][n]);
return 0;
}
(順便說一句,考試時巨集定義Max和Min沒加括號,直接導致這題爆零,正確的巨集定義應為:#define Min(a, b) ((a) < (b) ? (a) : (b))。)
如何來優化呢?
定義兩個dp,一個記錄從上往下走的答案,一個記錄從下往上走的答案。
狀態轉移方程:
- 只可以向右走,而不能向左走,就是上一層的路徑轉換到本層即可
\(f[i][j][1]=f[i][j][0]=\max(f[i-1][j][1],f[i-1][j][0])+a[i][j]\) - 向下走
\(f[i][j][1]=max(f[i][j][1],f[i][j−1][1]+a[i][j])\) - 向下走
\(f[i][j][0]=max(f[i][j][0],f[i][j+1][0]+a[i][j])\)
C++程式碼
#include <cstdio>
#define LL long long
#define INF 1e17
#define Max(a, b) ((a) > (b) ? (a) : (b))
void Quick_Read(LL &Number) {
Number = 0;
char c = getchar();
LL op = 1;
while (c < '0' || c > '9') {
if (c == '-')
op = -1;
c = getchar();
}
while (c >= '0' && c <= '9') {
Number = (Number << 1) + (Number << 3) + c - 48;
c = getchar();
}
Number *= op;
}
const LL MAXN = 1e3 + 5;
LL Map[MAXN][MAXN], dp[MAXN][MAXN][2];
LL n, m;
int main() {
Quick_Read(n);
Quick_Read(m);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
Quick_Read(Map[i][j]);
dp[i][j][0] = dp[i][j][1] = -INF;
}
}
dp[1][1][0] = dp[1][1][1] = Map[1][1];
for(int i = 2; i <= n; i++)
dp[i][1][1] = dp[i - 1][1][1] + Map[i][1];
for(int j = 2; j <= m; j++) {
dp[1][j][1] = dp[1][j][0] = Max(dp[1][j - 1][0], dp[1][j - 1][1]) + Map[1][j];
for(int i = 2; i <= n; i++) {
dp[i][j][0] = Max(dp[i][j - 1][0], dp[i][j - 1][1]) + Map[i][j];
dp[i][j][1] = Max(dp[i][j][0], dp[i - 1][j][1] + Map[i][j]);
}
for(int i = n - 1; i > 0; i--)
dp[i][j][0] = Max(dp[i][j][0], dp[i + 1][j][0] + Map[i][j]);
}
printf("%lld", Max(dp[n][m][0], dp[n][m][1]));
return 0;
}