2022-2023 ICPC Northwestern European Regional Programming Contest (NWERC 2022)
===
比賽鏈結
---
https://codeforces.com/gym/104875
比賽影片
---
{%youtube VsYpz2c1pd8%}
記分板
---

題解
---
### A. Alternating Algorithm
---
#### 題目摘要
#### 想法
:::spoiler 實作
```cpp=
```
:::
### B. Bottle Flip
---
#### 題目摘要
一圓柱裡有空氣和水,給你高、半徑、空氣密度及水密度,求最低重心發生時的水高度
#### 想法
帶公式三分搜
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define rep(a, b, c) for (int a = b; a < c; ++a)
#define pii pair<int, int>
#define all(x), (x).begin(), (x).end()
ld h, _, da, dw;
const ld eps = 1e-6;
ld calc(ld x){
return (x * x * dw + (h - x) * (h - x) * da) / (x * dw + (h - x) * da);
}
int main() {
cin >> h >> _ >> da >> dw;
ld l = 0, r = h;
while (r - l > eps) {
ld ml = l + (r - l) / 3, mr = r - (r - l) / 3;
if (calc(ml) < calc(mr)) r = mr;
else l = ml;
}
cout << fixed << setprecision(10) << l << '\n';
}
```
:::
### C. Circular Caramel Cookie
---
#### 題目摘要
給一個面積$s$,求最小半徑使得鬆餅上的格子**大於**$s$。
#### 想法
對半徑二分搜後,花$\sqrt s$的時間檢查就好,想有夠久,太笨。
切記大於
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define rep(a, b, c) for (int a = b; a < c; ++a)
#define pii pair<int, int>
#define all(x) (x).begin(), (x).end()
#define pb push_back
const ld pi = acos(-1);
const ld eps = 2e-6;
int main() {
ll s; cin >> s;
ld l = 1, r = 1000000;
while (r - l > eps) {
ll tot = 0;
ld mid = (l + r) / 2;
rep (i, 1, 1000000) {
if (i > mid) break;
tot += (ll)sqrtl(mid * mid - double(i * i));
}
tot *= 4;
if (tot > s) r = mid;
else l = mid;
}
cout << fixed << setprecision(10);
cout << r << '\n';
}
```
:::
### D. Delft Distance
---
#### 題目摘要
一張$h\times w$的圖中每格會是圓形或正方形,求從左上到右下不切到圓形或正方形的最短路
#### 想法
對每個格子點的一半多建點,並對所有點對它右、右下及下面的點建邊權,並用dp轉移
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define rep(a, b, c) for (int a = b; a < c; ++a)
#define pii pair<int, int>
#define all(x), (x).begin(), (x).end()
#define pi acos(-1)
const int N=1005;
const ld INF=1e18;
char s[N][N];
ld dis[4][N<<1][N<<1];
ld ans[N<<1][N<<1];
int main() {
int n, m;
cin >> n >> m;
for(int i=0;i<n;i++) for(int j=0;j<m;j++){
cin >> s[i][j];
if (s[i][j]=='X'){
dis[0][i<<1][j<<1]=0.5;
dis[1][i<<1][j<<1]=INF;
dis[2][i<<1][j<<1]=0.5;
dis[0][i<<1][j<<1|1]=0.5;
dis[1][i<<1][j<<1|1]=1.0;
dis[2][i<<1][j<<1|1]=INF;
dis[0][i<<1|1][j<<1]=INF;
dis[1][i<<1|1][j<<1]=1.0;
dis[2][i<<1|1][j<<1]=0.5;
}else{
dis[0][i<<1][j<<1]=0.5;
dis[1][i<<1][j<<1]=INF;
dis[2][i<<1][j<<1]=0.5;
dis[0][i<<1][j<<1|1]=0.5;
dis[1][i<<1][j<<1|1]=pi/4;
dis[2][i<<1][j<<1|1]=INF;
dis[0][i<<1|1][j<<1]=INF;
dis[1][i<<1|1][j<<1]=pi/4;
dis[2][i<<1|1][j<<1]=0.5;
}
}
for(int i=0;i<n;i++){
dis[2][i<<1][m<<1]=0.5;
dis[2][i<<1|1][m<<1]=0.5;
}
for(int j=0;j<m;j++){
dis[0][n<<1][j<<1]=0.5;
dis[0][n<<1][j<<1|1]=0.5;
}
for(int i=0;i<=(n<<1);i++) for(int j=0;j<=(m<<1);j++) ans[i][j]=INF;
ans[0][0]=0;
for(int i=0;i<=(n<<1);i++){
for(int j=0;j<=(m<<1);j++){
if (ans[i][j]>=INF) continue;
if (dis[0][i][j]!=INF) ans[i][j+1]=min(ans[i][j+1], ans[i][j]+dis[0][i][j]);
if (dis[1][i][j]!=INF) ans[i+1][j+1]=min(ans[i+1][j+1], ans[i][j]+dis[1][i][j]);
if (dis[2][i][j]!=INF) ans[i+1][j]=min(ans[i+1][j], ans[i][j]+dis[2][i][j]);
}
}
cout << fixed << setprecision(10)<< ans[n<<1][m<<1]*10 << '\n';
}
```
:::
### E. ETA
---
#### 題目摘要
給$\frac{p}{q}, \ \gcd(p, q) = 1$,構造出一個$n$點$m$邊的圖使得每個點走到$1$的最短路期望值為$\frac{p}{q}$
#### 想法
注意到我們最終只要構出一棵樹就好。
由於能輸出的$n$有一定限制,不妨去枚舉所有可能的$n$,也就是去枚舉每個$q$的倍數,這時候得到的資訊$\ p'\,$為$1$到所有點的路徑長度和
考慮最長能構出來的路徑和為一條鍊,也就是$\frac{(n - 1)n}{2}$,如果這個小於$p'$那就爛掉,再考慮最短的路徑和為全部都深度$1$,如果$p'<n-1$也爛掉。
接著就保證有解了,只要從一條鍊開始,好好將深度從深到淺做一些移動來彌補跟$p'$差距就好。
如果一直找都沒有就無解。
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define rep(a, b, c) for (int a = b; a < c; ++a)
#define pii pair<int, int>
#define all(x) (x).begin(), (x).end()
#define pb push_back
const ld pi = acos(-1);
int main() {
ll a, b; scanf("%lld/%lld", &a, &b);
for (int n = b; n <= 1000000; n += b) {
ll dis = 1LL * a * (n / b), cur = 1LL * (n - 1) * n / 2;
if (dis > cur || dis < n - 1) continue;
// cout << cur << ' ' << dis << '\n';
ll d = cur - dis;
vector<int> dep(n);
iota(all(dep), 0);
for (int i = n - 1; i > 1; --i) {
if (d == 0) continue;
int ok = dep[i] - 1;
if (d <= ok) {
dep[i] -= d;
d = 0;
break;
} else {
dep[i] = 1;
d -= ok;
}
}
vector<vector<int>> layer(n);
rep (i, 0, n) {
layer[dep[i]].pb(i);
}
cout << n << ' ' << n - 1 << '\n';
rep (i, 1, n) {
if (layer.empty()) break;
for (int x : layer[i]) {
cout << layer[i - 1].back() + 1 << ' ' << x + 1 << '\n';
}
}
exit(0);
}
cout << "impossible\n" << '\n';
}
```
:::
### F. Faster Than Light
---
#### 題目摘要
#### 想法
:::spoiler 實作
```cpp=
```
:::
### G. Going in Circles
---
#### 題目摘要
#### 想法
:::spoiler 實作
```cpp=
```
:::
### H. High-quality Tree
---
#### 題目摘要
對於一個二元平衡樹,定義為所有點的左子樹最深深度與右子樹最深深度相差不超過1。給你一顆二元樹,求最少要刪多少點使其變成二元平衡樹
#### 想法
利用分治的想法,先遞迴左子樹使其變平衡樹,再做右子樹,最後看兩者的最深深度(不含被刪除點),若兩者差>1,對深度深的子樹刪適當的點,因為每個點被刪掉次數只有一次,所以直接暴力刪點就好,複雜度$O(n)$
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
vector<int> v[N];
int ch[2][N];
int dep[N], mxd[N], cnt[N];
bool del[N];
int ans;
void dfs(int x, int p, int d){
cnt[x]=1;
dep[x]=mxd[x]=d;
for(auto e:v[x]) if (e!=p){
if (ch[0][x]==0) ch[0][x]=e;
else ch[1][x]=e;
dfs(e, x, d+1);
cnt[x]+=cnt[e];
mxd[x]=max(mxd[x], mxd[e]);
}
}
void dfs3(int x, int d){
if (del[x]) return;
if (dep[x]>=d){
del[x]=1;
ans++;
}
if (!del[ch[0][x]]) dfs3(ch[0][x], d);
if (!del[ch[1][x]]) dfs3(ch[1][x], d);
}
void dfs2(int x){
int d0=dep[x], d1=dep[x];
if (!del[ch[0][x]]) dfs2(ch[0][x]), d0=mxd[ch[0][x]];
if (!del[ch[1][x]]) dfs2(ch[1][x]), d1=mxd[ch[1][x]];
if (d0<d1) swap(d0, d1), swap(ch[0][x], ch[1][x]);
if (d0-d1>1){
dfs3(ch[0][x], d1+2);
}
mxd[x]=min(d0, d1+1);
}
int main(){
int n;
cin >> n;
for(int i=1;i<n;i++){
int a, b;
cin >> a >> b;
v[a].push_back(b);
v[b].push_back(a);
}
ans=0;
del[0]=1;
dfs(1, 0, 1);
dfs2(1);
cout << ans << '\n';
}
```
:::
### I. Interview Question
---
#### 題目摘要
若一數是$x$的倍數要輸出Fizz,是$y$的倍數數出Buzz,是兩者的倍數輸出FizzBuzz,否則輸出該數,給你$l到r$的輸出,輸出任意一組$x,\ y$
#### 想法
若Fizz(FizzBuzz)出現2次以上,則$x$是最近的兩者差,若出現1次,直接將$x$設為該數,否則直接設成$r+1$,Buzz同理
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define rep(a, b, c) for (int a = b; a < c; ++a)
#define pii pair<int, int>
#define all(x), (x).begin(), (x).end()
int main() {
int l, r; cin >> l >> r;
int a1=-1, b1=-1, a2=-1, b2=-1;
for(int i=l;i<=r;i++){
string s;
cin >> s;
if (s=="Fizz"){
if (a1==-1) a1=i;
else if (a2==-1) a2=i;
}else if (s=="Buzz"){
if (b1==-1) b1=i;
else if (b2==-1) b2=i;
}else if (s=="FizzBuzz"){
if (a1==-1) a1=i;
else if (a2==-1) a2=i;
if (b1==-1) b1=i;
else if (b2==-1) b2=i;
}
}
if (a1==-1&&a2==-1){
cout << r+1 << ' ';
}else if (a2==-1){
cout << a1 << ' ';
}else{
cout << a2-a1 << ' ';
}
if (b1==-1&&b2==-1){
cout << r+1 << '\n';
}else if (b2==-1){
cout << b1 << '\n';
}else{
cout << b2-b1 << '\n';
}
}
```
:::
### J. Justice Served
---
#### 題目摘要
給好多區間,該區間的答案是所有包覆該區間的答案的最大值+1,位被任何區間包覆答案是0,求所有區間答案
#### 想法
先對左端排序,若有人包覆我代表在我之前有人的右端比我的右端大,因此開個線段樹,藉由右界以後的最大值找到目前這線段的答案,再對右界位置單點改成線段答案就好
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> pii;
vector<int> v;
inline int id(int x){
return lower_bound(v.begin(), v.end(), x)-v.begin()+1;
}
const int N=4e5+10;
int t[N<<2];
vector<tuple<int, int, int>> p;
int ans[N];
void modify(int v, int l, int r, int x, int k){
if (x==l&&x==r){
t[v]=k;
return;
}
int m=l+r>>1;
if (x<=m) modify(v<<1, l, m, x, k);
else modify(v<<1|1, m+1, r, x, k);
t[v]=max(t[v<<1], t[v<<1|1]);
}
int query(int v, int l, int r, int ql, int qr){
if (ql<=l&&r<=qr) return t[v];
int m=l+r>>1;
int res=0;
if (ql<=m) res=max(res, query(v<<1, l, m, ql, qr));
if (qr>m) res=max(res, query(v<<1|1, m+1, r, ql, qr));
return res;
}
int main(){
int n;
cin >> n;
for(int i=0;i<n;i++){
int a, t;
cin >> a >> t;
v.push_back(a);
v.push_back(a+t);
p.push_back({a, a+t, i});
}
sort(v.begin(), v.end());
sort(p.begin(), p.end(), [](auto a, auto b){
auto [l1, r1, i1]=a;
auto [l2, r2, i2]=b;
if (l1==l2) return r1>r2;
return l1<l2;
});
int sz=v.size();
for(auto [l, r, idx]:p){
ans[idx]=query(1, 1, sz, id(r), sz)+1;
modify(1, 1, sz, id(r), ans[idx]);
}
for(int i=0;i<n;i++) cout << ans[i]-1 << ' ';
cout << '\n';
}
```
:::
### K. Kebab Pizza
---
#### 題目摘要
$N$塊披薩及$K$種配料,每塊披薩上有兩種配料(可能相同),加配料時必須連續加一段環狀區間,給你每個披薩上的配料(無序)問能不能達成。
#### 想法
轉換為圖論問題,把披薩看做邊,配料看做點。
顯然,一開始可以先將垃圾重邊給去掉。
考慮一種impossible的情況:如果一個點$v$連著三個以上非葉節點的點,那麼他一定是爛的。
原因是在任兩塊含$v$披薩中都會需要放其他配料的披薩,因此配料$v$無法連續。
接下來,會發現需要用到環狀性質的只有環,如果出現其他連通塊會幹到他用結尾連起來$(ex: (1, 2),\, (2, 3),\, (3, 1),\, (4, 5))$
因此若出現一個環,整張圖必須為一塊連通圖,否則impossible。反之各連通塊都無環就OK。
特別注意幾個很靠北的地方:
1. 自環
2. 若連到自環請注意自環不是葉節點
3. 會有沒有度數的點,不要在判連通圖時考慮它
4. 自環不是沒有度數的點
:::spoiler 實作
```cpp=
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define rep(a, b, c) for (int a = b; a < c; ++a)
#define pii pair<int, int>
#define all(x) (x).begin(), (x).end()
#define pb push_back
const int MAXN = 1e5 + 5;
int pa[MAXN], sz[MAXN], edge[MAXN];
int find(int x) {
return x == pa[x] ? pa[x] : pa[x] = find(pa[x]);
}
bool Union(int a, int b) {
a = find(a), b = find(b);
edge[a]++;
if (a == b) return false;
sz[a] += sz[b];
edge[a] += edge[b];
pa[b] = a;
return true;
}
int main() {
int n, k; cin >> n >> k;
vector<pii> tp(n);
for (auto &[a, b] : tp) {
cin >> a >> b;
a--, b--;
if (a > b) swap(a, b);
}
sort(all(tp));
tp.erase(unique(all(tp)), tp.end());
vector<vector<int>> adj(k);
iota(pa, pa + k, 0);
fill(sz, sz + k, 1);
for (auto [a, b] : tp) {
adj[a].pb(b);
if (a != b) {
adj[b].pb(a);
Union(a, b);
}
}
rep (i, 0, k) {
if (adj[i].size() >= 3) {
int cnt = 0;
for (int x : adj[i]) {
if (x == i) continue;
cnt += adj[x].size() > 1;
}
if (cnt >= 3) {
cout << "impossible\n";
exit(0);
}
}
}
int cyc = 0, cc = 0;
rep (i, 0, k) if (i == find(i)) {
if (adj[i].empty()) continue;
cc++;
cyc += sz[i] <= edge[i];
}
if (cc <= 1 || cyc == 0) cout << "possible\n";
else cout << "impossible\n";
}
```
:::
### L. Last Guess
---
#### 題目大綱
玩wordle,保證過程是好的以及有解,玩了n-1次後,輸出有可能的其中一組解
#### 想法
對於一組解答,會有以下的限制:
* 綠色代表一定是那個字母
* 黑色代表這個字母不可能在這裡出現
* 一個字母出現的次數下界會是在所有詢問中(綠色+黃色)最多的次數
* 其中,若一個字母在一次詢問中同時出現了綠黃黑,代表這個字母在答案出現的次數會恰好[上界 = 下界]是(綠+黃)
因此,可以考慮建最大流模型:
每個位置連邊到匯點流量為1,對於每個字母,對每個位置去記錄他可不可以放在這,可以就連流量為1的邊。
最後要來處理出現上下界問題,可以考慮多建一個點s',先將源點向每個字母建流量為字母下界的邊,再從s'向每個字母建(上界-下界)流量的邊,最後從s往s'建流量為$m - \sum {下界}$的邊就能很好的完成下界流的要求,
構造答案就檢查字母和位置中間的流量有沒有被流滿就好。
:::spoiler 實作
```cpp=
#pragma GCC optimize("O3,unroll-loops")
// #pragma GCC target("avx,popcnt,sse4,abm")
#include<bits/stdc++.h>
using namespace std;
#define ZTMYACANESOCUTE ios_base::sync_with_stdio(0), cin.tie(0)
#define ll long long
#define ull unsigned long long
#define pb push_back
#define all(a) (a).begin(), (a).end()
#define debug(x) cerr << #x << " = " << x << '\n';
#define rep(X, a, b) for(int X = a; X < b; ++X)
#define pii pair<int, int>
#define pll pair<ll, ll>
#define pld pair<ld, ld>
#define ld long double
#define F first
#define S second
pii operator + (const pii &p1, const pii &p2) { return make_pair(p1.F + p2.F, p1.S + p2.S); }
pii operator - (const pii &p1, const pii &p2) { return make_pair(p1.F - p2.F, p1.S - p2.S); }
pll operator + (const pll &p1, const pll &p2) { return make_pair(p1.F + p2.F, p1.S + p2.S); }
pll operator - (const pll &p1, const pll &p2) { return make_pair(p1.F - p2.F, p1.S - p2.S); }
template<class T> bool chmin(T &a, T b) { return (b < a && (a = b, true)); }
template<class T> bool chmax(T &a, T b) { return (a < b && (a = b, true)); }
#define lpos pos << 1
#define rpos pos << 1 | 1
template<typename A, typename B> ostream& operator<<(ostream &os, const pair<A, B> &p) { return os << '(' << p.first << "," << p.second << ')'; }
template<typename A> ostream& operator << (ostream &os, const vector<A> &p) { for(const auto &a : p) os << a << " "; os << '\n'; return os; }
const int MAXN = 2e5 + 5, MOD = 998244353, IINF = 1e9 + 7, MOD2 = 1000000007;
const double eps = 1e-9;
const ll LINF = 1e18L + 5;
const int B = 320;
// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
// int get_rand(int l, int r){ return uniform_int_distribution<int>(l, r)(rng); }
ll fpow(ll x, ll exp, ll mod = LLONG_MAX){ ll res = 1; while(exp){ if(exp & 1) res = res * x % mod; x = x * x % mod; exp >>= 1;} return res; }
template <typename T>
struct Dinic {
const T INF = numeric_limits<T>::max() / 2;
struct edge {
int v, r; T rc;
};
vector<vector<edge>> adj;
vector<int> dis, it;
Dinic(int n) : adj(n), dis(n), it(n) {}
void add_edge(int u, int v, T c) {
adj[u].pb({v, adj[v].size(), c});
adj[v].pb({u, adj[u].size() - 1, 0});
}
bool bfs(int s, int t) {
fill(all(dis), IINF);
queue<int> q;
q.push(s);
dis[s] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (const auto& [v, r, rc] : adj[u]) {
if (dis[v] < IINF || rc == 0) continue;
dis[v] = dis[u] + 1;
q.push(v);
}
}
return dis[t] < IINF;
}
T dfs(int u, int t, T cap) {
if (u == t || cap == 0) return cap;
for (int &i = it[u]; i < (int)adj[u].size(); ++i) {
auto &[v, r, rc] = adj[u][i];
if (dis[v] != dis[u] + 1) continue;
T tmp = dfs(v, t, min(cap, rc));
if (tmp > 0) {
rc -= tmp;
adj[v][r].rc += tmp;
return tmp;
}
}
return 0;
}
T flow(int s, int t) {
T ans = 0, tmp;
while (bfs(s, t)) {
fill(all(it), 0);
while ((tmp = dfs(s, t, IINF)) > 0) {
ans += tmp;
}
}
return ans;
}
bool inScut(int u) { return dis[u] < IINF; }
};
void solve() {
int n, m; cin >> n >> m;
vector<int> hi(26, m), lo(26, 0);
vector no(26, vector<bool>(m, 0));
vector<bool> vis(m, 0);
auto yes = no;
rep (i, 0, n - 1) {
vector<int> g(26, 0), b(26, 0), y(26, 0);
vector<int> cnt(26, 0);
string s, t; cin >> s >> t;
rep (j, 0, m) {
if (t[j] == 'G') {
vis[j] = 1;
yes[s[j] - 'a'][j] = 1;
cnt[s[j] - 'a']++;
} else if (t[j] == 'B') {
no[s[j] - 'a'][j] = 1;
} else {
no[s[j] - 'a'][j] = 1;
cnt[s[j] - 'a']++;
}
}
rep (j, 0, 26) {
if (count(all(s), 'a' + j) != cnt[j]) {
lo[j] = hi[j] = cnt[j];
} else {
chmax(lo[j], cnt[j]);
}
}
}
Dinic<int> G(26 + m + 3);
int s = 26 + m, t = s + 1, s2 = t + 1;
int tot = 0;
rep (i, 0, 26) {
G.add_edge(s, i, lo[i]);
G.add_edge(s2, i, hi[i] - lo[i]);
tot += lo[i];
rep (j, 0, m) {
if (yes[i][j]) {
G.add_edge(i, 26 + j, 1);
continue;
}
if (no[i][j] || vis[j]) continue;
G.add_edge(i, 26 + j, 1);
}
}
rep (i, 0, m) G.add_edge(26 + i, t, 1);
G.add_edge(s, s2, m - tot);
G.flow(s, t);
string ans(m, '#');
rep (i, 0, 26) {
for (auto [v, r, rc] : G.adj[i]) {
if (v != s && v != s2 && rc == 0) ans[v - 26] = char('a' + i);
}
}
cout << ans << '\n';
}
int main() {
ZTMYACANESOCUTE;
int T = 1;
// cin >> T;
while (T--) {
solve();
}
}
```
:::