本場金牌為超低罰時六題,穩拿金牌需要做出第七題。
但是我只會六題,這裡是前六題的題解。
ICPC2023香港站
J:
簽到但不是完全簽到,需要講。
首先每個位置只會走一次,所以讓 \(a_i\) 加一的操作只會在第一次到達某個位置時連續施行。
讓 \(a_i\) 加一再跳轉需要花費一個時間,讓 \(a_i\) 加二再跳轉需要花費兩個時間,你可以理解成先走到 \(i+a_i\) 的位置,再花費 1 的時間往後走一個位置,也可以花費 2 的時間往後走兩個位置。所以我們把每一個位置往下一個位置連花費為1的邊就好了。
但這個連邊是需要一個前提的,我們得能夠先走到 \(i\) 的位置,這些向後連的邊才有意義。比如終點是1,但是 \(a_0=2\),你是不能一步到達的。
解決方案很簡單,不要把0當初始點,而是把 \(a_0\) 當初始點,因為走到終點是至少需要跳這一步的。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
ll T;
ll n,X;
ll ans,a[N];
struct E{
ll to,we;
};
vector <E> e[N];
struct D{
ll id,di;
};
bool operator < (D A,D B) { return A.di > B.di; }
priority_queue <D> q;
ll dis[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
inline void add(ll u,ll v,ll z) {
e[u].push_back({v,z});
}
void DIJ() {
memset(dis,0x3f,sizeof(dis));
q.push({a[0],0}); dis[a[0]] = 0;
while(!q.empty()) {
D now = q.top(); q.pop();
ll u = now.id;
if(dis[u]!=now.di) continue;
FOR() {
ll v = e[u][i].to, w = e[u][i].we;
if(dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
q.push({v,dis[v]});
}
}
}
}
int main() {
n = read(); X = read();
for(ll i=0;i<n;i++) a[i] = read(), add(i,(a[i]+i)%n,1);
for(ll i=0;i<n;i++) add(i,(i+1)%n,1);
DIJ();
cout<<dis[X]+1;
return 0;
}
A:
一道很像網路流的矩陣選數題。題面中的 NP-complete 把我隊友給誤導了,其實解法很簡單。
和之前一道類似的矩陣構造題很像,先全部改成一致顏色,然後只需要考慮一個方向。
這題的解法也是類似,先把所有的1改成0,之後修改任意位置其實都是加一。為了同時滿足行和列的要求,有一個貪心的選數方法,比較經典。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int N=501010;
const int qwq=303030;
const int inf=0x3f3f3f3f;
int T;
int n,m;
int a[4040][4040],b[4040][4040];
char s[N];
int hang[N];
struct E{
int id,zhi;
}lie[N];
inline bool cmp(E A,E B) { return A.zhi < B.zhi; }
inline int read() {
int sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int main() {
n = read();
for(int i=1;i<=n;i++) lie[i].id = i;
for(int i=1;i<=n;i++) {
scanf("%s",s+1);
for(int j=1;j<=n;j++) {
if(s[j]=='-') a[i][j] = -1;
else {
a[i][j] = 1;
hang[i]--;
lie[j].zhi--;
}
}
}
for(int i=1;i<=n;i++) hang[i] += read();
for(int i=1;i<=n;i++) lie[i].zhi += read();
for(int i=1;i<=n;i++) {
if(hang[i]>0) { cout<<"No"; return 0; }
sort(lie+1,lie+n+1,cmp);
for(int j=1;j<=(-hang[i]);j++) lie[j].zhi++, b[i][lie[j].id] = 1;
}
// bool ke = 1;
for(int i=1;i<=n;i++) if(lie[i].zhi!=0) { cout<<"No"; return 0; }
cout<<"Yes\n";
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
if(a[i][j]==1) {
if(b[i][j]) cout<<0;
else cout<<1;
}
else {
if(b[i][j]) cout<<1;
else cout<<0;
}
}
cout<<endl;
}
return 0;
}
I:
當你的重置能力CD好了之後,你有兩個選擇:一是等你的普通攻擊CD,CD好了之後先攻擊,再重置,然後立即攻擊;二是馬上重置,然後攻擊。
其他的選擇,你手玩一下會發現是愚昧的。
而這兩個選擇結果都是相同的,都會使得兩個技能重新開始冷卻(其實就和重新開始一個情況了)。
這兩種選擇,週期不一樣,攻擊次數不一樣。
資料範圍允許我們列舉第二種選擇的數量,然後推算出第一種選擇的數量,剩餘部分一直平A。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
ll T;
ll n,m;
ll A,B;
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
int main() {
T = read();
while(T--) {
A = read(); B = read(); m = read();
ll k = B/A;
if(B%A==0) {
cout<<(m/A+m/B+2)*160<<"\n";
continue;
}
k++;
ll res = 0;
for(ll i=0;i<=m;i+=B) {
ll wo = k*(i/B);
ll sheng = m-i;
ll ans = wo + sheng/(k*A) + sheng/A + 2;
res = max(res,ans);
}
cout<<res*160<<"\n";
}
return 0;
}
D:
每個點入度上限為3,簡單多了。
首先一個點不能連三個紅邊,也不能連三個藍邊,所以紅邊的連通塊只能是鏈或環。
然後發現連成環也允許,一個紅色環至少需要三個點,這些點無法透過藍邊連起來。
所以藍色連通塊和紅色連通塊都是鏈。
然後發現,鏈的長度還不能超過四,也就是說最多四個點,因為一個點想要成為紅點的鏈中意味著它要連兩個紅邊,只能連一個藍邊,它就是藍邊的鏈頭或鏈尾,我們不允許三個鏈頭鏈尾出現,所以紅鏈只能有兩個鏈中,鏈長度最多是四。
然後就是分類討論,一個點,兩個點,三個點,四個點。四個情況。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
ll T;
ll n,m;
ll ans;
vector <ll> e[N],d[N];
map <ll,ll> f,g;
ll si[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
ll check(ll A,ll C) {
if(!A || !C) return 0;
return (f[A*n+C] && g[A*n+C]);
}
int main() {
ll x,y,z;
n = read(); m = read();
for(ll i=1;i<=m;i++) {
x = read(); y = read(); z = read();
if(z==1) {
e[x].push_back(y);
e[y].push_back(x);
f[x*n+y] = f[y*n+x] = 1;
}
else {
d[x].push_back(y);
d[y].push_back(x);
g[x*n+y] = g[y*n+x] = 1;
}
}
for(ll i=1;i<=n;i++) if(e[i].size()==3 || d[i].size()==3) si[i] = 1;
ans = n;
ll res = 0;
for(auto v : f) { if(g[v.first]) res++; }
ans += res / 2;
res = 0;
ll A, B, C, D;
for(ll i=1;i<=n;i++) {
if(si[i]) continue;
A = B = C = D = 0;
if(e[i].size()==2) A = e[i][0], B = e[i][1];
if(e[i].size()==1) A = e[i][0];
if(d[i].size()==2) C = d[i][0], D = d[i][1];
if(d[i].size()==1) C = d[i][0];
res += check(A,C) + check(B,C) + check(A,D) + check(B,D);
}
ans += res;
res = 0;
for(ll i=1;i<=n;i++) {
if(e[i].size()!=1) continue;
A = i; B = e[i][0];
if(e[B].size()!=2) continue;
if(e[B][0]==A) C = e[B][1];
else C = e[B][0];
if(e[C].size()!=2) continue;
if(e[C][0]==B) D = e[C][1];
else D = e[C][0];
if(e[D].size()!=1) continue;
if(g[A*n+B] && g[A*n+D] && g[C*n+D]) res++;
if(g[A*n+C] && g[A*n+D] && g[B*n+D]) res++;
}
ans += res / 2;
cout<<ans;
return 0;
}
H:
妙妙樹上DP,我寫的這個是一個很奇怪的做法。
首先要發現一點:雖然車是按從小到大一個一個尋找空位的,但我們不需要關心它們的具體順序,只要是一個合法的方案,我們調換任意兩輛車的順序,依舊是合法的。
所以我們dp時就不需要考慮車的編號了,只需要考慮每個點上有幾輛車。
最後的答案如何統計,假如第 \(i\) 個點停了 \(b_i\) 輛車,那麼這樣的方案對應到車輛編號的情況數就有 \(\frac{n!}{b_1!b_2!...b_n!}\) 種,也就是超排列。我們不能讓最後的方案數直接乘以 \(n!\) 就是因為有些車是在同一個點的,直接乘以 \(n!\) 會重複計算這一部分。我們 dp 出來的方案數不好統計究竟有多少車在同一個點,但我們發現可以把 \(b_i!\) 分母的這部分先計入我們的dp,也就是說,只要選了一個大小為 \(b_i\) 的點(\(b_i\) 輛車在這個點),我們就讓 dp 結果除以 \(b_i\),最後再乘上一個 \(n!\) 就是最終答案了。
dp 方程怎麼設,怎麼轉移,需要觀察題目的性質。
因為題目中的過程所有車都是根向走的,我們將時間倒流,每個點上有一輛車,所有車往葉向走回到初始時刻,你會發現一個性質,每一棵子樹中車的數量一定不小於子樹大小,而且多出來的部分也不會超過這棵子樹到根的距離。題目所給的 “隨機資料” 的性質就在這裡體現了,每個點到根的距離期望值不會超過log。
我們設 \(f[u][i]\) 表示 u 這棵子樹中車比點多了 i 個的方案數,兒子們多出來的點數加起來就是父親多出來的點數,所以這是一個累加求和的揹包問題。合併完之後我們要選擇 u 這個父親結點上車的數量,這時我們要讓答案除以 \(b_u\),具體含義就是上一段所講的內容。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() ll le=e[u].size();for(ll i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
using namespace std;
const ll N=501010;
const ll qwq=303030;
const ll inf=0x3f3f3f3f;
const ll p=998244353;
ll T;
ll n,m;
vector <ll> e[N];
ll dep[N];
ll f[N][123],g[123];
ll F[N],ni[N];
inline ll read() {
ll sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
inline ll ksm(ll aa,ll bb) {
ll sum = 1;
while(bb) {
if(bb&1) sum = sum * aa %p;
bb >>= 1; aa = aa * aa %p;
}
return sum;
}
void qiu() {
F[0] = ni[0] = 1;
for(ll i=1;i<=N-10;i++) F[i] = F[i-1] * i %p;
ni[N-10] = ksm(F[N-10],p-2);
for(ll i=N-11;i>=1;i--) ni[i] = ni[i+1] * (i+1) %p;
}
void DFS(ll u) {
FOR() {
ll v = e[u][i];
dep[v] = dep[u] + 1;
DFS(v);
}
}
void TREE(ll u) {
for(ll v : e[u]) TREE(v);
f[u][0] = 1;
ll now = 0;
for(ll v : e[u]) {
memset(g,0,sizeof(g));
for(ll j=0;j<=dep[v];j++) {
for(ll k=0;k<=now;k++) {
if(j+k>dep[u]+1) break;
(g[j+k] += f[u][k] * f[v][j] %p) %= p;
}
}
now += dep[v];
for(ll j=0;j<=min(now,dep[u]+1);j++) f[u][j] = g[j];
}
memset(g,0,sizeof(g));
for(ll i=0;i<=dep[u]+1;i++) {
ll duo = i-1;
for(ll j=0;j<=now;j++) if(duo+j>=0 && duo+j<=dep[u]) (g[duo+j] += f[u][j] * ni[i] %p) %= p;
}
for(ll i=0;i<=dep[u];i++) f[u][i] = g[i];
}
int main() {
int x;
qiu();
n = read();
for(ll i=2;i<=n;i++) {
x = read();
e[x].push_back(i);
}
DFS(1);
TREE(1);
cout<<(f[1][0]*F[n])%p;
return 0;
}
E:
這題我們捏了個很有趣的東西哈哈,我們稱之為 “左偏笛卡爾樹”。
我們的思路是醬紫的:
首先看字典序最小的拓撲排序,我們找到最大的那個數字,連一條邊讓它指向右邊的整體,表示原圖中肯定是先訪問了這個點才能訪問右邊的那些點,否則這個數字就不會出現在這裡。
而它左邊的哪些數字呢,我們把它們視作並列的兄弟關係,因為即使它們之間相互不連邊,在最小拓撲排序中數大的點依舊是後訪問。
因此我們可以遞迴地構建一棵樹:找到區間中最大的數,左邊的整體成為它的兄弟,右邊的整體成為它的兒子。
左右區間各找最大點成為兒子,這樣構造的樹是標準的笛卡爾樹,而我們這個是右邊成為兒子,左邊成為兄弟,我們形象地稱之為 “左偏笛卡爾樹”。
然後是最大的拓撲排序,原理是一樣的,我們繼續構造一棵樹,把兩棵樹的所有邊加在一起,就是答案的圖。(這裡要注意第二棵樹的邊往第一棵樹里加時,不允許出現右邊的點連向左邊這種情況,可以用第一棵樹的dfn序來判斷,若有這種邊直接不合法)
正確性怎麼證明呢?
首先是必要性:想要得到題目所給的最小最大拓撲序,我們必須存在這些邊。因為我們建圖就是為了滿足這樣的性質,有這些邊才能導致這樣的順序。
然後是充分性:只要我們有了這兩棵樹的所有邊,我們就能得到題目所給的最小最大拓撲序。因為在任意一棵樹種不存在右側連向左側的邊,且兄弟結點從左到右依次增大,我們總是會先訪問左側的點,然後是左側點的兒子(它們比父親結點優先順序更高,因而肯定也要比父親的右兄弟優先順序高),進而訪問右側點,因此一定會得到題目所給的順序。
證畢。
程式碼實現:遞迴造樹,RMQ或線段樹查詢區間最大最小值。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define FOR() int le=e[u].size();for(int i=0;i<le;i++)
#define QWQ cout<<"QwQ\n";
#define ll long long
#include <vector>
#include <queue>
#include <map>
#define ls now<<1
#define rs now<<1|1
using namespace std;
const int N=801010;
const int qwq=303030;
const int inf=0x3f3f3f3f;
int T;
int n,m;
int a[N],b[N];
struct E{
int mx,mi,idx,idi;
}t[N<<2],ling;
vector <int> e[N];
int du[N];
int vis[N];
int st1[N],st2[N],cnt;
int dfn[N],tim;
inline int read() {
int sum = 0, ff = 1; char c = getchar();
while(c<'0' || c>'9') { if(c=='-') ff = -1; c = getchar(); }
while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
return sum * ff;
}
void add(int u,int v) {
e[u].push_back(v);
du[v]++;
}
E pushup(E A,E B) {
E C = ling;
if(A.mx > B.mx) C.mx = A.mx, C.idx = A.idx;
else C.mx = B.mx, C.idx = B.idx;
if(A.mi < B.mi) C.mi = A.mi, C.idi = A.idi;
else C.mi = B.mi, C.idi = B.idi;
return C;
}
void built(int now,int l,int r) {
if(l==r) { t[now] = {a[l],a[l],l,l}; return ; }
int mid = l+r >> 1;
built(ls, l, mid);
built(rs, mid+1, r);
t[now] = pushup(t[ls],t[rs]);
}
E query(int now,int l,int r,int x,int y) {
if(x<=l && r<=y) return t[now];
E res = ling;
int mid = l+r >> 1;
if(x<=mid) res = pushup( query(ls, l, mid, x, y), res );
if(y>mid) res = pushup( query(rs, mid+1, r, x, y), res );
return res;
}
void solve(int fa,int l,int r,int cl) {
if(l>r) return ;
E wo = query(1, 1, n, l, r);
if(cl==1) {
add(fa,wo.mx);
solve(wo.mx, wo.idx+1, r, cl);
solve(fa, l, wo.idx-1, cl);
}
else {
if(dfn[wo.mi]<dfn[fa]) { cout<<"No\n"; exit(0); }
add(fa,wo.mi);
solve(wo.mi, wo.idi+1, r, cl);
solve(fa, l, wo.idi-1, cl);
}
}
void DFS(int u) {
vis[u] = 1;
FOR() {
int v = e[u][i];
if(vis[v]) continue;
du[v]--;
if(!du[v]) DFS(v);
}
}
void TREE(int u) {
dfn[u] = ++tim;
for(int i=e[u].size()-1;i>=0;i--) {
TREE(e[u][i]);
}
}
int main() {
ling = {-inf,inf,0,0};
n = read();
for(int i=1;i<=n;i++) {
a[i] = read();
}
built(1, 1, n);
solve(0, 1, n, 1);
TREE(0);
for(int i=1;i<=n;i++) {
a[i] = read();
}
built(1, 1, n);
solve(0, 1, n, 2);
for(int i=0;i<=n;i++) {
if(!du[i] && !vis[i]) DFS(i);
}
for(int i=1;i<=n;i++) if(!vis[i]) {cout<<"No\n"; return 0;}
for(int i=1;i<=n;i++) {
for(int v : e[i]) {
st1[++cnt] = i; st2[cnt] = v;
}
}
cout<<"Yes\n";
cout<<cnt<<"\n";
for(int i=1;i<=cnt;i++) cout<<st1[i]<<" "<<st2[i]<<endl;
return 0;
}