AtCoder Regular Contest 177
A-Exchange
問題陳述
判斷 \(n\) 個價格分別為 \(x_i\) 的商品,問能否透過有限數量的 \(1\) 元, \(5\) 元, \(10\) 元, \(50\) 元, \(100\) 元, \(500\) 元,購買。
思路
貪心。
每個商品從 \(500\) 元開始,能用就儘量用。如果中間某個商品無法被滿足,則無解,反之有解。
證明就憑感覺想一下。如果前面用大面值,就可以留下小面值的給後面填空子。
程式碼
太醜了懶得放了。
B - Puzzle of Lamps
問題陳述
AtCoder 先生建立了一個由 \(N\) 個小燈泡(從左到右排列成一排)和兩個開關 A 和 B 組成的裝置:0"(關)和 "1"(開)。按下每個開關會產生以下結果:
- 按下開關 A 會將最左邊處於 "0 "狀態的燈泡變成 "1"。
- 按下開關 B 會將處於 "1 "狀態的最左邊燈泡變為 "0"。
如果沒有適用的燈泡,則無法按下開關。
最初,所有燈泡都處於 "0 "狀態。他希望燈泡的狀態從左到右為 \(S_1, S_2, \dots, S_N\) 。請確定按下開關的順序和次數。按動次數不一定要最少,但最多應為 \(10^6\) ,以便在實際時間內完成操作。可以證明,在該問題的約束條件下存在一個解。
思路
先考慮 0010
如何解決。
注意到一組可行解是 AAABB
。
看到這裡可能有點感覺了,先用A把每個 \(1\) 前面都填滿 \(1\),在用B把前面多餘的 \(1\) 退掉。
再考慮 0101
如何解決。
注意到一組可行解為 AAABBBAAB
。
發現可以先解決右邊,再解決左邊,這樣可以把每個 \(1\) 拆開做,會方便更多。
最後考慮 00111000011
如何解決。
注意到一組可行解為 AAAAAAAAAAABBBBBBBBBAAAAABB
。
一段 \(1\),可以先用A把 \(1\) 延申到右端點,再用B把左端點前的 \(1\) 退掉。
從右到左解決每個區間。
這樣構造出的解長度是 \(n^2\) 級別的,不會超限。
程式碼
#include <bits/stdc++.h>
using namespace std;
int n, cnt;
char s[35];
struct seg {int l, r;} sg[35];
vector <char> ans;
int main() {
scanf("%d", &n);
scanf("%s", s + 1);
for (int i = 1, l, r; i <= n; i ++) {
if (s[i] == '1' && s[i - 1] != '1') l = i;
if (s[i] == '1' && s[i + 1] != '1') r = i, sg[++ cnt] = {l, r};
}
for (int i = cnt; i >= 1; i --) {
for (int j = 1; j <= sg[i].r; j ++) ans.emplace_back('A');
for (int j = 1; j < sg[i].l; j ++) ans.emplace_back('B');
}
printf("%d\n", ans.size());
for (auto c : ans) putchar(c);
return 0;
}
C - Routing
問題陳述
有一個網格,網格中有 \(N\) 行和 \(N\) 列。設 \((i, j)\) \((1 \leq i \leq N, 1 \leq j \leq N)\) 表示位於從上往下第 \(i\) 行和從左往上第 \(j\) 列的單元格。每個單元格最初被塗成紅色或藍色,如果 \(c_{i,j}=\) R
單元格 \((i, j)\) 被塗成紅色,如果 \(c_{i,j}=\) B
單元格 \((i, j)\) 被塗成藍色。您想將某些單元格的顏色改為紫色,以便同時滿足以下兩個條件:
條件 1:從單元格 \((1, 1)\) 移動到單元格 \((N, N)\) 時,只能經過紅色或紫色的單元格。
條件 2:您只能透過藍色或紫色單元格,才能從單元格 \((1, N)\) 移動到單元格 \((N, 1)\) 。
這裡的 "您可以移動"是指您可以透過重複移動到水平或垂直相鄰的相關顏色的單元格,從起點到達終點。
要滿足這些條件,最少有多少個單元格必須變為紫色?
思路
廣搜或最短路板子。
搜一遍或跑一遍,求出 \((1,1)\) 到 \((N,N)\) 最少經過多少藍色,\((1,N)\) 到 \((N,1)\) 最少經過多少紅色,相加即答案。這裡給出最短路程式碼。
程式碼
#include <bits/stdc++.h>
using namespace std;
char s[505][505];
int n, b[505][505], r[505][505];
bool vis[505][505];
int xz[] = {1, 0, -1, 0};
int yz[] = {0, 1, 0, -1};
struct node {int x, y, d;};
bool operator < (node a, node b) {
return a.d > b.d;
}
priority_queue <node> q;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i ++) scanf("%s", s[i] + 1);
memset(b, 0x3f, sizeof(b));
memset(r, 0x3f, sizeof(r));
b[1][1] = (s[1][1] == 'B');
q.push({1, 1, b[1][1]});
while (!q.empty()) {
node now = q.top(); q.pop();
if (vis[now.x][now.y]) continue;
vis[now.x][now.y] = 1;
if (now.x == n && now.y == n) continue;
for (int i = 0; i < 4; i ++) {
int xx = now.x + xz[i], yy = now.y + yz[i];
if (xx < 1 || xx > n || yy < 1 || yy > n) continue;
if (b[xx][yy] > now.d + (s[xx][yy] == 'B')) {
b[xx][yy] = now.d + (s[xx][yy] == 'B');
q.push({xx, yy, b[xx][yy]});
}
}
}
memset(vis, 0, sizeof(vis));
r[n][1] = (s[n][1] == 'R');
q.push({n, 1, r[n][1]});
while (!q.empty()) {
node now = q.top(); q.pop();
if (vis[now.x][now.y]) continue;
vis[now.x][now.y] = 1;
if (now.x == 1 && now.y == n) continue;
for (int i = 0; i < 4; i ++) {
int xx = now.x + xz[i], yy = now.y + yz[i];
if (xx < 1 || xx > n || yy < 1 || yy > n) continue;
if (r[xx][yy] > now.d + (s[xx][yy] == 'R')) {
r[xx][yy] = now.d + (s[xx][yy] == 'R');
q.push({xx, yy, r[xx][yy]});
}
}
}
printf("%d\n", b[n][n] + r[1][n]);
return 0;
}
D - Earthquakes
問題陳述
AtCoder 街是一條在平地上用直線表示的道路。路上豎立著 \(N\) 根電線杆,高度為 \(H\) 。電線杆按時間順序編號為 \(1, 2, \dots, N\) 。電線杆 \(i\) ( \(1 \leq i \leq N\) )垂直於座標 \(X_i\) 。每根電線杆的底座都固定在地面上。
街道將經歷 \(N\) 次地震。在 \(i\) /-次地震 \((1 \leq i \leq N)\) 中,會發生以下事件:
- 如果電線杆 \(i\) 尚未倒下,它會倒向數字線的左邊或右邊,每個機率為 \(\frac{1}{2}\) 。
-
- 如果一根倒下的電線杆與另一根尚未倒下的電線杆相撞(包括在電線杆底部相撞),後一根電線杆也會朝同一方向倒下。這可能會引發連鎖反應。
在步驟 1 中,一根電線杆倒下的方向與其他電線杆倒下的方向無關。
下圖是在一次地震中電線杆可能倒下的示例:
為了防備地震,對於每一次 \(t = 1, 2, \dots, N\) ,求出在 \(t\) /-次地震中所有極點都倒下的機率。將其乘以 \(2^N\) ,並列印出結果的模數 \(998244353\) 。可以證明要列印的值是整數。
思路
1.分析
我們發現,所有的電線杆可以被劃分為若干段。
定義一段為左端點的電線杆向右倒或右端點的電線杆向左倒均能讓整段電線杆全部倒完的極長子區間。
不同段之間不會有任何影響。因此,我們可以將原問題拆分為子問題:
有一段位置升序的電線杆,每個電線杆在第 \(p_i\) 次地震中倒塌,求最後倒塌的電線杆是第 \(i\) 的機率。
2.解決子問題
2.1分析子問題
我們發現任何時刻,一段區間均可被分成三部分:
向左倒塌的一部分,站立的一部分,向右倒塌的一部分。
第 \(i\) 個電線杆未倒塌,當且僅當所有 \(j < i,p_j<p_i\) 的 \(j\) 都向左倒塌,所有 \(j>i,p_j<p_i\) 的 \(j\) 都向右倒塌。
第 \(i\) 個電線杆是最後倒塌的,當且僅當 \(i\) 未倒塌且 \(i\) 是站立的一段的起點或終點。
2.2分析機率
我們發現,第 \(i\) 個電線杆是最後倒塌的機率為 \(1/2^a\times b/2\)。
其中 \(a\) 為 \(j < i,p_j<p_i\) 或 \(j>i,p_j<p_i\) 的 \(j\) 的個數。
這些 \(j\) 代表了在 \(i\) 之前倒塌的電線杆。因為必須保持 \(i\) 站立,所以左邊必須往左倒,右邊必須往右倒,機率為 \(1/2^a\)。
若 \(i\) 為站立區間的左端點或右端點,\(b=1\)。
若 \(i\) 是單獨的一個(即既是左端點又是右端點),\(b=2\)。
若 \(i\) 是左右端點中的一個,則 \(i\) 倒下的方向有要求,機率為 \(1/2\)。
若 \(i\) 同時是左右端點(單獨),則 \(i\) 倒下的方向沒有要求,機率為 \(1\)。
2.3求解機率
我們發現,機率中 \(a\) 的求解過程為單調棧的板子,\(b\) 的解決平凡。
至此,子問題已經解決,時間複雜度 \(O(l)\),\(l\) 為段的長度。
3.合併子問題
設一共有 \(c\) 段,電線杆 \(i\) 所在的段的編號為 \(g_i\)。
時間 \(t\) 的答案為 \(s_1\times s_2\times \ldots \times s_{g_t-1}\times x \times s_{g_t+1} \times \ldots \times s_c\)。
其中 \(x\) 為子問題 \(g_t\) 中最後倒下的電線杆是 \(t\) 的機率,
\(s_i\) 為子問題 \(i\) 中最後倒下的電線杆編號小於 \(t\) 的機率之和。
直接樸素計算是 \(O(n^2)\) 。
我們發現這是一個單點修改,區間查詢問題,考慮使用線段樹最佳化。
線段樹中維護 \(s\),每次將答案算出後,將 \(x\) 加到 \(s_{g_t}\) 中。
時間複雜度 \(O(n\log n)\)。
實現細節
實現時題目要求要將答案乘上 \(2^N\),但我們解決子問題時不能乘 \(2^N\),而要乘 \(2^l\),這樣所有子問題乘起來才是 \(2^N\)。
程式碼
#include <bits/stdc++.h>
#define int long long
const int mod = 998244353;
const int N = 2e5 + 5;
using namespace std;
struct segt {
struct node {int l, r, v;} t[N << 2];
#define ls (p << 1)
#define rs (p << 1 | 1)
void build(int p, int l, int r) {
t[p].l = l, t[p].r = r, t[p].v = 0;
if (l == r) return ;
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
}
void add(int p, int id, int v) {
if (t[p].l == t[p].r) {
t[p].v += v, t[p].v %= mod;
return ;
}
if (id <= t[ls].r) add(ls, id, v);
else add(rs, id, v);
t[p].v = t[ls].v * t[rs].v, t[p].v %= mod;
}
int query(int p, int l, int r) {
if (l <= t[p].l && t[p].r <= r) return t[p].v;
int res = 1;
if (t[ls].r >= l) res *= query(ls, l, r), res %= mod;
if (t[rs].l <= r) res *= query(rs, l, r), res %= mod;
return res;
}
} T; // 線段樹板子
struct Point {int x, y;};
bool cmp(Point a, Point b) {return a.x < b.x;}
int n, h, c, x[N], g[N], t[N], k[N], pow2[N];
Point a[N];
vector <int> p[N];
vector <int> res[N];
void solve(int id) { // 解決子問題
int m = p[id].size() - 1;
stack <int> stk;
for (int i = 1; i <= m; i ++) {
while (!stk.empty() && // 單調棧板子
p[id][i] < stk.top()) stk.pop();
stk.push(p[id][i]);
k[i] = stk.size() - 1;
}
stack <int> sstk;
for (int i = m; i >= 1; i --) {
while (!sstk.empty() && // 單調棧板子
p[id][i] < sstk.top()) sstk.pop();
sstk.push(p[id][i]);
k[i] += sstk.size() - 1;
}
res[id].emplace_back(0);
for (int i = 1; i <= m; i ++) { // 計算機率
int b = (i == 1 || p[id][i - 1] < p[id][i])
+ (i == m || p[id][i] > p[id][i + 1]);
res[id].emplace_back(b * pow2[m - k[i] - 1] % mod); // 乘2^l
}
}
signed main() {
cin >> n >> h, pow2[0] = 1;
for (int i = 1; i <= n; i ++) {
cin >> x[i];
a[i].x = x[i], a[i].y = i;
pow2[i] = (pow2[i - 1] << 1) % mod;
}
for (int i = 1; i <= n; i ++)
p[i].emplace_back(0);
sort(a + 1, a + n + 1, cmp);
g[a[1].y] = ++ c,
p[c].emplace_back(a[1].y),
t[a[1].y] = p[c].size() - 1;
for (int i = 2; i <= n; i ++) { // 分段
if (a[i].x - a[i - 1].x <= h)
g[a[i].y] = c,
p[c].emplace_back(a[i].y),
t[a[i].y] = p[c].size() - 1;
else g[a[i].y] = ++ c,
p[c].emplace_back(a[i].y),
t[a[i].y] = p[c].size() - 1;
}
for (int i = 1; i <= c; i ++) solve(i); // 解決子問題
T.build(1, 1, c);
for (int i = 1; i <= n; i ++) { // 合併答案
int x = res[g[i]][t[i]], ans = 1;
if (g[i] - 1)
ans *= T.query(1, 1, g[i] - 1);
if (g[i] + 1 <= c)
ans *= T.query(1, g[i] + 1, c), ans %= mod;
ans *= x, ans %= mod;
T.add(1, g[i], x);
cout << ans << ' ';
}
return 0;
}