Editorial for Educational Round 94 (Div. 2)
涉及知識點:模擬、貪心、列舉、遞迴、動態規劃、AC自動機、組合計數
A. String Similarity
不能再水的送分題?有挺多個做法。
首先我們觀察到每個要求都包含 \(s_n\),那我們把所有 \(w_i\) 都搞成 \(s_n\) 就一定滿足要求。
#include <bits/stdc++.h> using namespace std; int t; int n; char s[200]; int main() { scanf("%d", &t); while (t--) { scanf("%d", &n); scanf("%s", s + 1); for (int i = 1; i <= n; i++) printf("%d", s[n] - '0'); printf("\n"); } return 0; }
還有其它的構造方法,比如:\(w_i=s_{2i-1}\)。這樣倒過來構造也對。
B - RPG Protagonist
直接貪心是不對的,我們觀察題目性質,發現 \(cnt_s\) 很小,這就在提示我們列舉。
有一個貪心還是對的,就是錢少的那個顯然買的越多越好。我們不妨設 S 是錢少的那個。
那麼我們可以列舉第一個人買了多少 S,這樣第二個買了多少也就知道,他們分別還剩多少錢也就清楚了,很容易算出還可以買多少 W。
#include <bits/stdc++.h> using namespace std; int t; int p, f; int cs, cw; int s, w; int ans; int main() { cin >> t; while (t--) { ans = 0; cin >> p >> f; cin >> cs >> cw; cin >> s >> w; if (s > w) swap(s, w), swap(cs, cw); int i = min(p / s + f / s, cs); for (int j = 0; j <= i && s * j <= p; j++) { int tmpp = p - s * j; int tmpf = f - s * (i - j); if (tmpf < 0) continue; ans = max(ans, i + min(tmpp / w + tmpf / w, cw)); } cout << ans << "\n"; } return 0; }
C - Binary String Reconstruction
一道不錯的模擬題,可是放在2C這個位置是不是有點水?
首先我們顯然可以確定 \(w_i\) 是不是 \(0\),因為如果 \(s_{i+x}=0\) 或 \(s_{i-x}=0\) 則 \(w_i\) 一定是 \(0\)。否則令 \(w_i=1\)。
再判斷一下是否有 \(s_i=1\) 但是 \(w_{i-x}=0\) 且 \(w_{i+x}=0\) 的情況,如果出現則答案為 \(-1\)。出現這種情況的原因是 \(s_{i-2x}=0\) 且 \(s_{i+2x}=0\)。
#include <bits/stdc++.h> using namespace std; int t; int n, x; char s[100010], w[100010], ans[100010]; int main() { cin >> t; while (t--) { scanf("%s", s + 1); cin >> x; n = strlen(s + 1); for (int i = 1; i <= n; i++) { if (i - x > 0 && s[i - x] == '0') w[i] = '0'; else if (i + x <= n && s[i + x] == '0') w[i] = '0'; else w[i] = '1'; } bool flag = 1; for (int i = 1; i <= n; i++) { if (s[i] == '1') { int cnt = 0; if (i + x <= n && w[i + x] != '0') cnt++; if (i - x > 0 && w[i - x] != '0') cnt++; if (!cnt) flag = 0; } } if (!flag) { puts("-1"); continue; } for (int i = 1; i <= n; i++) cout << w[i]; cout << "\n"; } return 0; }
D - Zigzags
看到資料範圍很容易想到列舉兩個位置。關鍵是列舉哪兩個位置,其實列舉任意兩個位置都是可的但是程式碼複雜度各不相同。
我是嘗試列舉 \(i,k\),同時我們統計每個位置之後的每種數有多少個,記為 \(cnt_{i,v}\)。
然後開始列舉,先列舉 \(i\),然後列舉 \(k\),在擴充套件的時候記錄 \(i\rightarrow k\) 這一段內每種數字的個數,記作 \(num_v\),每擴充套件一個 \(k\),我們需要知道在 \((i,k)\) 和 \((k,n]\) 範圍內的共同數字的個數,記為 \(cur\)。那麼顯然數字 \(a_k\) 和之前的都不能算了,所以要減去 \(num_{a_k}\)。如果 \(a_i=a_k\) 則更新答案。接下來又多了個 \(a_k\),令 \(cur+=cnt[k][a[k]]\),同時 \(num_{a_k}\) 要自增 \(1\)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
template <typename T> void read(T &x) {
T f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for(x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
x *= f;
}
int t;
int n;
int a[3010], cnt[3010][3010], num[3010];
ll ans;
int main() {
read(t);
while (t--) {
ans = 0;
read(n);
for (int i = 1; i <= n; i++) read(a[i]);
for (int i = 1; i <= n + 5; i++) for (int j = 1; j <= n + 5; j++) cnt[i][j] = 0;
for (int i = n - 1; i >= 1; i--) {
for (int j = 1; j <= n; j++) cnt[i][j] = cnt[i + 1][j];
cnt[i][a[i + 1]]++;
}
for (int i = 1; i <= n; i++) {
ll cur = 0;
for (int j = i + 1; j <= n; j++) {
cur -= num[a[j]];
if (a[i] == a[j]) ans += cur;
cur += cnt[j][a[j]];
num[a[j]]++;
}
for (int j = i + 1; j <= n; j++) num[a[j]]--;
}
printf("%lld\n", ans);
}
return 0;
}
賽後發現列舉 \(j,k\) 要簡單好多。不過我相信列舉 \(j,k\) 的題解會很多,我這篇題解只是給列舉 \(i,k\) 的但是不知道自己哪裡錯了的人查錯的。另外這也是一種思路。
E - Clear the Multiset
老套路題了,這種題貌似真的很多。考慮一個區間,我們有兩種消的辦法:
- 全部用方法二
- 先用方法一消到最小值,然後遞迴去求
肯定不會有人腦抽先用幾次方法一再用方法二吧。。。
那麼直接按照上面的模擬就好。因為區間不會重複,所以不需要記憶化。
對於第二種消法,我們只需找到一個最小值,根據這個去分割槽間就行。然後遞迴求解時記得加一個當前已被消了多少。關於時間複雜度,我們可以把這個遞迴過程看做一棵樹,這個樹最多有 \(n\) 層,每層以為區間不重疊所以是 \(O(n)\) 的,總的時間複雜度即為 \(O(n^2)\)。還可以通過 rmq,笛卡爾樹等做到 \(O(n\log{n})\),\(O(n)\)。
#include <bits/stdc++.h>
using namespace std;
int n;
int a[5010];
int solve(int l, int r, int lst) {
if (l > r) return 0;
if (l == r) return a[l] > lst;
int mi = l;
for (int i = l + 1; i <= r; i++) if (a[i] < a[mi]) mi = i;
return min(r - l + 1, solve(l, mi - 1, a[mi]) + solve(mi + 1, r, a[mi]) + a[mi] - lst);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
cout << solve(1, n, 0);
return 0;
}
F - x-prime Substrings
AC自動機好題。賽時我一直以為是字首和亂搞,賽後看題解發現是AC自動機覺得很妙。
觀察到 \(x\) 很小,我們跑一遍暴力發現所有 x-prime 數的長度的和最大也不超過 \(5000\)(實測應該是 \(x=19\) 時最大)。那問題就變成了總長度不超過 \(5000\) 的模式串和一個文字串問最少從文字串中刪去幾個字元可以使得文字串不包含任意一個模式串。
多串匹配問題想到AC自動機。這種問題又可以想到動態規劃。設 \(dp_{i,j}\) 代表匹配到文字串第 \(i\) 位且當前在AC自動機上的狀態 \(j\)。首先下個位置肯定可以刪掉,所以 \(dp_{i+1,j}\) 可以為 \(dp_{i,j}+1\)。然後考慮不刪下一個字元,則加上下一個字元後一定不能是某個模式串結尾,這可以在 \(fail\) 樹上 \(dp\) 求得某個狀態是不是某個模式串的結尾。如果不是,則其可以為 \(dp_{i,j}\)。最後的答案就是 \(min(dp_{n,所有狀態})\)。
#include <bits/stdc++.h>
using namespace std;
char s[1010];
int n, x;
int t[25], cnt;
int trie[5010][10], tot = 1;
int fail[5010];
queue<int> q;
int dp[1010][5010];
bool End[5010];
bool check() {
for (int i = 1; i <= cnt; i++) {
int now = 0;
for (int j = i; j <= cnt; j++) {
if (i == 1 && j == cnt) continue;
now += t[j];
if (x % now == 0) return false;
}
}
return true;
}
void dfs(int now) {
if (now == 0) {
if (!check()) return;
int p = 1;
for (int i = 1; i <= cnt; i++) {
if (!trie[p][t[i]]) trie[p][t[i]] = ++tot;
p = trie[p][t[i]];
} End[p] = true;
return;
}
for (int i = 1; i <= 9; i++) {
if (i <= now) {
t[++cnt] = i;
dfs(now - i);
cnt--;
}
}
}
int main() {
scanf("%s%d", s + 1, &x);
n = strlen(s + 1);
dfs(x);
fail[1] = 0;
for (int i = 1; i <= 9; i++) trie[0][i] = 1;
q.push(1);
while (!q.empty()) {
int p = q.front();
q.pop();
End[p] |= End[fail[p]];
for (int i = 1; i <= 9; i++) {
if (trie[p][i]) {
fail[trie[p][i]] = trie[fail[p]][i];
q.push(trie[p][i]);
} else {
trie[p][i] = trie[fail[p]][i];
}
}
}
memset(dp, 0x3f, sizeof(dp));
dp[0][1] = 0;
for (int i = 0; i < n; i++) {
for (int j = 1; j <= tot; j++) {
dp[i + 1][j] = min(dp[i + 1][j], dp[i][j] + 1);
if (!End[trie[j][s[i + 1] - '0']]) {
dp[i + 1][trie[j][s[i + 1] - '0']] = min(dp[i + 1][trie[j][s[i + 1] - '0']], dp[i][j]);
}
}
}
int ans = 0x3f3f3f3f;
for (int i = 1; i <= tot; i++) ans = min(ans, dp[n][i]);
printf("%d", ans);
return 0;
}
G - Mercenaries
先咕著。