在這份筆記中,我們說明APCS 2024年6月實作題考試的題目解說。這些題目是根據ZeroJudge上的版本,與真實考試的版本題意應該是一樣的,但題目敘述、測資與環境並不一定相同。
每一題的題解都包含題意分析、演算法、C/C++範例程式與Python範例程式。其中後兩個單元是獨立的,也就是必要的說明會在兩個單元重複出現,為了方便有些人只看其中一種語言。
(題目連結)1.特技表演
在一個正整數序列
我們由左往右歷遍全部的點,記錄著目前的下降區段的長度,當新的點進來時:
為了記錄最長的下降區段長度,我們另外用一個變數維護好目前所看到的最長區段長度。
我們不需要紀錄整個序列,因此以prev紀錄前一個高度,curr為目前看到的高度,ilen則是目前區段的長度。prev的初值給予不可能的小的-1,這樣寫比較方便。接著跑一個
在每一回合結束時,更新prev以及比對ilen是否比最長的區段更長,若是,更新之。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, i,ilen=0,prev=-1,curr=0,imax=0;
scanf("%d",&n);
for (i=0;i<n;i++) {
scanf("%d",&curr);
if (curr > prev) { // restart a new run
ilen = 1;
} else {
ilen++; // current length+1
}
prev = curr;
if (ilen > imax) imax = ilen;
}
printf("%d\n",imax);
return 0;
}
使用Python者需要對list有基本認識以方便處理輸入,本題一開始我們將所有輸入先讀入到一個list
以imax紀錄目前最長的區段,以ilen紀錄目前區段的長度,兩者的初值給予1代表目前
迴圈從1開始,第6行如果是高度下降,延伸目前的區段;否則,當作新區段的起點。第10行是嘗試更新最長區段的長度。
n = int(input())
h = [int(x) for x in input().split()]
imax = 1 # max length
ilen = 1 # current length
for i in range(1,n): # iterate the heights
if h[i] <= h[i-1]: # extend the current length
ilen += 1
else:
ilen = 1 # start a new decreasing segment
imax = max(imax,ilen) # update the best answer
#
print(imax)
這一題資料量很小,也可以用效率較差但更直覺的方法做。嘗試以每一個點
n = int(input())
h = [int(x) for x in input().split()]
imax = 0 # max length
for i in range(n): # try start at i
j = i+1
while j<n and h[j]<=h[j-1]:
j += 1
# decreasing = i ~ j-1
imax = max(imax,j-i) # update the best answer
#
print(imax)
(題目連結) 2.電子畫布
模擬在一個
這就是個模擬的題目,且相對簡單。我們以矩陣的橫列與直行來定義位置,
這個簡單的題目,C與C++差異不大,以下提供C的寫法。
#include <stdio.h>
#include <stdlib.h>
int main() {
int h,w,n, i, r,c,t,j,x,k;
int grid[25][25]={0};
scanf("%d%d%d",&h,&w,&n);
for (k=0;k<n;k++) {
scanf("%d%d%d%d",&r,&c,&t,&x);
for (i=r-t; i<=r+t; i++) {
for (j=c-t; j<=c+t; j++) {
if (i>=0 && i<h && j>=0 && j<w && abs(i-r)+abs(j-c)<=t)
grid[i][j] += x;
}
}
}
for (i=0; i<h; i++) {
for (j=0; j<w-1;j++) printf("%d ",grid[i][j]);
printf("%d\n",grid[i][w-1]);
}
return 0;
}
第1行讀入
第4行輸入一次著色的參數
最後是輸出,逐行將矩陣內容印出,印出一列可以簡單用第11行的指令即可。
h,w,n = [int(x) for x in input().split()]
grid = [[0]*w for i in range(h)]
for i in range(n):
r,c,t,x = [int(x) for x in input().split()]
for p in range(r-t,r+t+1):
for q in range(c-t,c+t+1):
if 0<=p<h and 0<=q<w and abs(p-r)+abs(q-c)<=t:
grid[p][q] += x
#
for row in grid:
print(*row)
(題目連結) 3. 缺字問題
本題大意是先給你一個字母的集合
例如,若給定的字母集為{ a, c, m } 且
使用很直覺的想法,既然要找出字典序最小的字串,我們就依照字典序一一產生所有長度
要實現以上想法有很多個做法,需要的技巧有兩個:
假設
先看第二點,C++與Python都有內建功能可以在一個字串中找尋另外一個字串,但是在本題中不適用,因為長度
因為要做多次的搜尋,將搜尋對象做預先處理來加快速度是應該有的思維。常用的方法是排序後二分搜,或者以字典(hash table)儲存後搜尋。這兩個方法在本題中應該都可以,不過以下我們介紹一種更加簡單且有效率的方法。
給定字母集以及長度之後,所有的字串可以依照字典序對應到0 ~
所以我們可以將
更方便的是,我們不需要產生所有字典序的字串,只要由0開始歷遍整數找到第一個不出現的
這個方法的時間複雜度是
由於C與C++對字串的處理有較大的差異,我們展示兩種寫法。
先展示C++的寫法。
第6行的chk是用來檢查的表格,這裡用bool,第7行的irank是將給定字母集的字母對應到0 ~
// encode to int
#include <bits/stdc++.h>
using namespace std;
#define N 500010
#define KL 600000
bool chk[KL]={false};
int irank[128]; // rank of char in alphabet
int main() {
string s, alpha;
int leng,i,j;
cin >> alpha >> leng >>s;
int n=s.length(), k = alpha.length(),kl=1,p=0;
for (i=0;i<leng;i++) kl *=k; // k^l
for (i=0;i<k;i++) irank[alpha[i]] = i; // rank of each alphabet
// encode all substring of leng
for (p=0,i=0; i<leng;i++) p = p*k+irank[s[i]]; // first substring
chk[p] = true;
for (i=leng; i<n; i++) { // substring ended at i
p = (p*k+irank[s[i]])%kl; // rolling encoding
chk[p] = true;
}
string ans(leng,'0');
for (i=0; i<kl && chk[i]; i++) ; // i = first missing code
// decode ans
j = leng-1;
while (i) {
ans[j--] = alpha[i%k];
i /= k;
}
while (j>=0) ans[j--] = alpha[0]; // leading 0
cout << ans <<'\n';
return 0;
}
第17 ~ 22行是將
第24行用一個for迴圈找到第一個沒出現的
下面是C的寫法,字串是字元陣列,這個部分與C++比較不同,其他大同小異。
// encode to int and dfs generate each
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 500010
#define KL 600000
char s[N],chk[KL]={0};
int rank[128]; // rank of char in alphabet
int main() {
char alpha[20];
int leng,i,j;
scanf("%s%d%s",alpha,&leng,s);
int n=strlen(s), k=strlen(alpha),kl=1,p=0;
for (i=0;i<leng;i++) kl *=k; // k^l
for (i=0;i<k;i++) rank[alpha[i]] = i; // rank of each alphabet
// encode all substring of leng
for (p=0,i=0; i<leng;i++) p = p*k+rank[s[i]]; // first substring
chk[p] = 1;
for (i=leng; i<n; i++) { // substring ended at i
p = (p*k+rank[s[i]])%kl; // rolling encoding
chk[p] = 1;
}
char ans[20];
for (i=0; i<kl && chk[i]; i++) ; // i = first missing code
// decode ans
j = leng-1;
while (i) {
ans[j--] = alpha[i%k];
i /= k;
}
while (j>=0) ans[j--] = alpha[0]; // leading 0
ans[leng] = '\0';
printf("%s\n",ans);
return 0;
}
以下介紹Python的範例。
第6行的變數kl是用來存
第9 ~ 14行是將
第15行用找到第一個沒出現的 i,本題保證一定存在,所以可以直接用index找出串列中的第一個。然後開始解碼,解碼的程序就是每次用i%k找出最後一位,然後//=k右移一位,這跟二進位轉換是一樣的程序。稍微要注意如果位數不足前面要補alpha[0],所以我們ans的初值給予全部alpha[0]。
此處留意Python的字串是不可修改的物件,我們先做list of string,然後用join把他們串接在一起。
# encode to int
alpha = input()
l = int(input())
s = input()
n,k = len(s),len(alpha)
kl = pow(k,l)
rank = {c:i for i,c in enumerate(alpha)}
chk = [False]*kl
code = 0 # encode s[:l]
for i in range(l): code = code*k+rank[s[i]]
chk[code] = True
for c in s[l:]: #rolling encoding
code = (code*k+rank[c])%kl
chk[code] = True
i = chk.index(False) # find the first missing i
# decode i
ans = [alpha[0]]*l # initial all alpha[0]
for j in range(l-1,-1,-1):
ans[j] = alpha[i%k]
i //= k
ans = "".join(ans)
print(ans)
由於Python的set/dict (hash table)是個很容易使用的東西,我們再給另外一種使用set的寫法。
第7行一行就把
alpha = input()
l = int(input())
s = input()
n = len(s)
k = len(alpha)
succ = {p:q for p,q in zip(alpha, alpha[1:])}
d = set(s[i:i+l] for i in range(n-l+1))
ans = alpha[0]*l
while True: #always found
if ans not in d:
break
# next
i = l-1
while ans[i]==alpha[-1]: # always found
i -= 1
ans = ans[:i]+succ[ans[i]]+alpha[0]*(l-i-1)
#print(ans)
#
print(ans)
(題目連結) 4.最佳選擇
有
所謂prefix(前綴)是指序列前方的連續一段,而suffix則是後面連續的一段。
有幾個條件限制,讓題目看起來比較混亂。其實找頭尾個一段與找中間一段是一樣的,因為兩者是互補的,找頭尾一段總和不超過lim就等價於找中間一段不小於
這類型題目的基本型是找出中間一段總和不超過lim的最大總和。
由於是正整數,單一個區間往左右延伸時,其總和必然是單調遞增的。所以很自然地可以用sliding window (或稱雙指針)的方法來做。基本觀念很簡單,我們始終關注一個區間(window)
「此區間是右端=
由於前面說到的單調性,假設前面一回合的迴圈不變性已經滿足,當右端往右移動一格時,區間的總和只會變大,它所對應到的最佳左端一定是大於等於之前的左端。我們只需要將左端往右移動到第一個讓總和不超過lim的位置即可。
演算法如下:
best = isum = 0
le = 1
for ri = 1 to n do
isum += a[ri]
while isum > lim do // move le
isum -= a[le]
le += 1
end while
best = max(best,isum)
end for
由於le與ri都只有往右移動,所以時間複雜度是線性的。
與基本型相比,本題有兩個變化:
其實原理還是一樣,為了滿足奇偶個數的條件,我們對每一個prefix與suffix以它們的奇數個數減去偶數個數(以下稱為dif值)次予以分類擺放,相同dif值的前綴和放在同一個串列。對於每一個後綴,它的dif值如果是
如何在對應的串列中找到最佳搭配的前綴呢?我們的目標是前綴與後綴的和不超過某個
當然不能用線性搜尋或枚舉比對,因為時間太慢。記得我們的前綴放入串列中都是單調遞增的,所以可以用二分搜來找。
事實上不需要,俗話說:「連續二分搜不如一路爬過去」,我們由小到大產生所有的後綴和,所以要找的前綴和在每個串列中是單調下降的。每次我們只要在對應的串列中比對最後一個前綴和,如果這個前綴和太大(
最後一個問題是如何儲存這些前綴和的串列呢?同一個串列的dif值是相同的,本題保證所有prefix的dif值的絕對值不超過
整理一下,算法流程如下:
注意到,每一個串列我們只在其後增加元素或刪除最後一個元素,所以其實是當做stack在使用。
我們用vector來放串列。第13行的bal是
有一個容易被忽略的細節,我們必須考慮空的prefix與後綴。第15行是對應到空的prefix,其和為0,dif值也是0。接下來16 ~ 21行算每一個prefix的和與dif,將其和放入bal[M+dif]中,由於總和是單調上升,所以放入後也是遞增序列。
第22行是個細節,如果lim大於整個序列的總和,我們將其改為總和,這是要避免後面搜到的prefix與suffix重疊了。
第25 ~ 26行是找出suffix為空時的解,第27行開始逐步推suffix,對於每一個suffix,我們到第
時間複雜度是
// prefix and suffix <=lim; bal[suffix] == -bal[prefix]
#include <bits/stdc++.h>
using namespace std;
#define N 300000
#define M 2000
int main() {
int n, i,j,k,lim;
scanf("%d%d",&n,&lim);
vector<int> v(n);
for (i=0;i<n;i++) scanf("%d",&v[i]);
// partition by odd-even
vector<int> bal[M+M+1]; // odd-even balance
int ans = n, psum=0,dif=0,ssum=0;
bal[M].push_back(0);
for (i=0;i<n;i++) {
psum += v[i];
if (v[i]&1) dif++;
else dif--;
bal[M+dif].push_back(psum);
}
if (lim>psum) lim=psum;
// sweep suffix
ssum =0, dif = 0;
while (bal[M].back()>lim) bal[M].pop_back();
ans = bal[M].back(); // for empty suffix
for (i=n-1;i>=0;i--) {
ssum += v[i];
if (v[i]&1) dif++;
else dif--;
if (abs(dif)>M) continue;
k = M-dif;
while (!bal[k].empty() && bal[k].back()+ssum > lim)
bal[k].pop_back();
if (!bal[k].empty()) ans=max(ans,ssum+bal[k].back());
}
printf("%d\n",ans);
//fprintf(stderr,"lim,ans=(%d,%d), max dif=%d, min dif=%d\n",lim,ans,dmax,dmin);
return 0;
}
這個題目用C來寫的時候會遇到一點小麻煩,在前面C++的實作中,我們用vector來存那些串列(堆疊),但C沒有可變長度的陣列,如果宣告
// prefix and suffix <=lim; bal[suffix] == -bal[prefix], c version
#include <stdio.h>
#define N 300010
#define M 2000
int v[N],psum[N],next[N],top[M+M+1];
int main() {
int n, i,j,k,lim;
scanf("%d%d",&n,&lim);
for (i=1;i<=n;i++) scanf("%d",&v[i]); // 1-index
for (i=0;i<M+M+1;i++) top[i]=-1;
int ans = 0, dif=0;
// partition by odd-even
psum[0] = 0;
next[0] = -1;
top[M] = 0; // push 0 (empty prefix)
for (i=1; i<=n; i++) {
if (v[i]&1) dif++;
else dif--;
psum[i] = psum[i-1]+v[i]; // prefix sum
if (dif>M || dif<-M) fprintf(stderr,"bound exceed\n");
next[i] = top[M+dif]; // push i into stack (M+dif)
top[M+dif] = i;
}
if (psum[n]<lim) lim=psum[n];
// sweep suffix
int ssum =0;
while (psum[top[M]] > lim) top[M] = next[top[M]];
ans = psum[top[M]]; // for empty suffix
dif = 0;
for (i=n; i>0; i--) {
ssum += v[i];
if (v[i]&1) dif++;
else dif--;
if (dif>M || dif<-M) continue;
k = M-dif; //
while (top[k]!=-1 && psum[top[k]]+ssum > lim)
top[k] = next[top[k]];
if (top[k]!=-1 && ssum+psum[top[k]]>ans)
ans = ssum+psum[top[k]];
}
printf("%d\n",ans);
return 0;
}
以下是Python的寫法。第4行的stk是
有一個容易被忽略的細節,我們必須考慮空的prefix與後綴。第5行是對應到空的prefix,其和為0,dif值也是0。接下來8 ~ 13行算每一個prefix的和與dif,將其和放入stk[M+dif]中,由於總和是單調上升,所以放入後也是遞增序列。
第14行是個細節,如果lim大於整個序列的總和,我們將其改為總和,這是要避免後面搜到的prefix與suffix重疊。
第17 ~ 18行是找出suffix為空時的解,第19行開始逐步推suffix,對於每一個suffix,我們到第
時間複雜度是
n,lim = [int(x) for x in input().split()]
v = [int(x) for x in input().split()]
M = 2000
stk = [[] for i in range(M+M+1)] # odd-even =0 at [M]
# partition prefix sum into stack
stk[M].append(0) # prefix sum=0
psum = dif = 0
for x in v:
psum += x
#if psum>lim: break
if x&1: dif += 1
else: dif -= 1
stk[M+dif].append(psum)
if lim>psum: lim = psum
# sweep suffix
ssum = dif = 0
while stk[M] and stk[M][-1]>lim: stk[M].pop()
ans = stk[M][-1]
for x in v[::-1]:
ssum += x
if x&1: dif += 1
else: dif -= 1
if abs(dif)>M: continue
while stk[M-dif] and stk[M-dif][-1]+ssum>lim:
stk[M-dif].pop()
if stk[M-dif]:
ans = max(ans,stk[M-dif][-1]+ssum)
#
print(ans)
End