ICPC2023南京站题解(A C D F G I L M)

本场金牌线为7题前一半。做出8题可稳金牌,这里是难度前8题的题解。

1|0ICPC2023南京站


1|1I:


签到题。

#include <bits/stdc++.h> #define ll long long #define QWQ cout<<"QwQ"<<endl; #define FOR() ll le=e[u].size();for(ll i=0;i<le;i++) using namespace std; const ll N=501010; 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 T; int n,m; struct E{ int wei,zhi; }a[N]; inline bool cmp(E A,E B) { return A.wei<B.wei; } int main() { T = read(); while(T--) { bool flag = 1; n = read(); m = read(); for(int i=1;i<=m;i++) { a[i].wei = read(); a[i].zhi = read(); } sort(a+1,a+m+1,cmp); for(int i=1;i<=m;i++) { if(a[i].wei-a[i-1].wei==a[i].zhi-a[i-1].zhi) continue; if(a[i].zhi<a[i].wei-a[i-1].wei) continue; flag = 0; break; } if(flag) cout<<"Yes\n"; else cout<<"No\n"; } return 0; }

1|2F:


等价重写,可以轻松地想到本题和图论有关,每个操作修改的数字是独一无二的,因此每个覆盖事件都可以产生一个先后关系,我们可以将某个位置先写入的数字连向后写入的数字,在拓扑排序中就可以表示先写入较早的数字。

我WA了一发哈哈,因为没考虑完整,连边只需要连向每个位置最后覆盖的数字就行,之前谁把谁覆盖没有关系。

#include <bits/stdc++.h> #define ll long long #define QWQ cout<<"QwQ"<<endl; #define FOR() ll le=e[u].size();for(ll i=0;i<le;i++) using namespace std; const ll N=501010; 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 T; int n,m; int a[N]; int ru[N]; vector <int> e[N]; int ans[N],cnt; priority_queue <int> q; vector <int> g[N]; void chushihua() { for(ll i=1;i<=n;i++) ru[i] = 0, e[i].clear(); for(ll i=1;i<=m;i++) g[i].clear(); cnt = 0; } int main() { int x,y; T = read(); while(T--) { chushihua(); n = read(); m = read(); for(int i=1;i<=n;i++) { x = read(); while(x--) { y = read(); g[y].push_back(i); } } for(int i=1;i<=m;i++) { if(g[i].size()==0) continue; int hou = g[i].back(); for(auto v : g[i]) if(v != hou) e[v].push_back(hou), ru[hou]++; } for(int i=1;i<=n;i++) if(!ru[i]) q.push(i); while(!q.empty()) { int u = q.top(); q.pop(); ans[++cnt] = u; FOR() { int v = e[u][i]; ru[v]--; if(ru[v]==0) q.push(v); } } bool tong = 1; for(int i=1;i<=cnt;i++) if(i!=ans[i]) tong = 0; if(tong) { cout<<"No\n"; continue; } cout<<"Yes\n"; for(int i=1;i<=n;i++) cout<<ans[i]<<" \n"[i==n]; } return 0; }

1|3C:


官方题解中给出了两种解法,都有思考价值。

解法一:

异或运算满足不等式:ababa+b,那么我们把题目中的 g 写出来:g=(kP+1)(P1),它也满足不等式:(k1)P+2(kP+1)(P1)(k+1)P

这个不等式能告诉我们什么?虽然我们的 (kP+1) 异或上了 (P1) 使得结果 g 的具体值不确定,但是 g 是有取值范围的,这个范围的大小不超过2P,随着 k 的增长,g 的取值范围左右各增加一个 P,也就是说,g 的取值范围接近上限 m 时,k 的取值就在 mP 左右,小于这个值时,g 一定比 m 小,大于这个值时 g 一定比 m 大,在 mP 左右时特判即可。

解法二:

这个做法是比较无脑且科技的,比较推荐:

将 [l,r] 这个区间的所有整数异或上一个值 X ,可以得到 log 段连续的区间。[0,m] 这个区间异或上 (P-1),每一段判断有多少 (kP+1) 即可。

#include <bits/stdc++.h> #define ll long long using namespace std; const ll N=101010; 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 T; ll m,P; struct D{ ll l,r; }d[N]; inline bool cmp(D A,D B) { return A.l < B.l; } ll cnt; void get_fen(ll R,ll X) { ll da = 1ll << 60ll; d[cnt=1] = {0,R}; for(ll i=60;i>=0;i--,da>>=1) { if(d[1].l+da<=d[1].r) { if((X>>i)&1) { d[++cnt] = {d[1].l+da,d[1].l+2*da-1}; d[1].r -= da; } else { d[++cnt] = {d[1].l,d[1].l+da-1}; d[1].l += da; } } else if((X>>i)&1) d[1].l += da, d[1].r += da; } sort(d+1,d+cnt+1,cmp); // for(ll i=1;i<=cnt;i++) cout<<d[i].l<<" "<<d[i].r<<"\n"; } inline ll ask(ll R) { if(R<=0) return 0; return (R-1)/P+1; } int main() { T = read(); while(T--) { P = read(); m = read(); get_fen(m,P-1); ll ans = 0; for(ll i=1;i<=cnt;i++) { ans += ask(d[i].r); ans -= ask(d[i].l-1); } cout<<ans<<"\n"; } return 0; }

1|4G:


可以免费选物品的背包,总感觉哪里做过。

假如从珠宝店出来,你拿着一些物品,把这些物品分为两类,一类是你买的,一类是你免费选的,那么可以肯定的是,你免费选的那些物品,一定是价格比较高的,这样省钱。

所以这两类物品是存在一个分界线的,是按价格排序的分界线。

枚举一个价格,设免费选的都高于这个价格,买来的都低于这个价格,那就高于这个价格的优先选价值高的,低于这个价格的用背包。

前者优先队列,后者朴素背包。

#include <bits/stdc++.h> #define ll long long #define QWQ cout<<"QwQ"<<endl; #define FOR() ll le=e[u].size();for(ll i=0;i<le;i++) using namespace std; const ll N=501010; 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 n,W,K; struct E{ ll w,v; }a[N]; inline bool cmp(E A,E B) { return A.w > B.w; } priority_queue <ll, vector<ll>, greater<ll> > q; ll qian[N]; ll f[N]; int main() { n = read(); W = read(); K = read(); for(ll i=1;i<=n;i++) { a[i].w = read(); a[i].v = read(); } sort(a+1,a+n+1,cmp); ll now = 0; for(ll i=1;i<=n;i++) { now += a[i].v; q.push(a[i].v); if(i>K) now -= q.top(), q.pop(); qian[i] = now; } ll ans = 0; for(ll i=n;i>=1;i--) { ll da = 0; for(ll j=W;j>=a[i].w;j--) { f[j] = max(f[j], f[j-a[i].w] + a[i].v); da = max(f[j], da); } ans = max(ans,da+qian[i-1]); } cout<<ans; return 0; }

1|5A:


南京站四次重现,还挺酷的。

题目比较吓人,但是稍微想想会发现,如果我是袋鼠,我会把我能走到的地方都走一遍,如果还没有赢,那我就赢不了了。

我能走的区域就是四连通块,如果场内有能够包含我这个连通块的其他连通块,那我就成为不了赢家,反之肯定能赢,且我连通块内的其他袋鼠也一定能赢(这个很关键)。

枚举所有的连通块,扫一遍全图,每个位置判断需要花费我连通块大小的时间,设每个连通块大小为 sizi ,总复杂度为 sizinm=(nm)2 ,因为题目中 nm 是 1000,所以平方可过。

#include <bits/stdc++.h> #define ll long long using namespace std; const int N=1010; const int inf=0x3f3f3f3f; 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 T; int n,m; char s[N]; int ff[] = {1,-1,0,0}; int gg[] = {0,0,1,-1}; int a[N][N]; int shang, xia, zuo, you; int vis[N][N]; struct D { int x,y; }st[N]; int cnt; int ans; void chushihua() { for(int i=0;i<=n+1;i++) for(int j=0;j<=m+1;j++) vis[i][j] = a[i][j] = 0; ans = 0; } void DFS(int x,int y) { st[++cnt] = {x,y}; vis[x][y] = 1; shang = min(shang,x); zuo = min(zuo,y); xia = max(xia,x); you = max(you,y); for(int k=0;k<4;k++) { int xx = ff[k] + x; int yy = gg[k] + y; if(vis[xx][yy] || !a[xx][yy]) continue; DFS(xx,yy); } } int main() { T = read(); while(T--) { chushihua(); n = read(); m = read(); for(int i=1;i<=n;i++) { scanf("%s",s+1); for(int j=1;j<=m;j++) { if(s[j]=='.') a[i][j] = 1; else a[i][j] = 0; } } for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { if(a[i][j] && !vis[i][j]) { cnt = 0; shang = zuo = inf; xia = you = 0; DFS(i,j); int shu = xia-shang+1; int heng = you-zuo+1; bool you = 0; for(int I=1;I<=n;I++) { if(I+shu-1>n) break; for(int J=1;J<=m;J++) { if(J+heng-1>m) break; if(I==shang && J==zuo) continue; bool can = 1; for(int k=1;k<=cnt;k++) { if(!a[ I+st[k].x-shang ][ J+st[k].y-zuo ]) { can = 0; break; } } if(can) {you = 1; break;} } if(you==1) break; } if(!you) ans += cnt; } } } cout<<ans<<endl; } return 0; }

1|6L:


这题思考难度是签到,但是难写程度甚至导致成为了银牌题。

电梯运包裹,因为包裹大小只有 1 和 2,且贪心满足要求,所以是可以排完序后直接模拟的,直接模拟的话可能会有些卡手,因为想使用两个set分别存 1 和 2 的包裹的话,来回选还是比较难写的。

我的做法把所有包裹放在一起按楼层排序,如何寻找最高的大小为 1 的包裹呢,想必大家都听说过树状数组二分这个东西吧?支持单点减(某个1包裹被运完),查询第k个数的值(查询楼层最大的1包裹的位置)。

但是我觉得麻烦没用树状数组,我是直接二分的,在前缀和上二分,找楼层最大的1包裹,因为答案是单调的,所以某个包裹被运完之后,我可以把二分的右区间改成这个楼层,这样就保证答案一定还没运完。

题解提供了避免区分 1 和 2 包裹的转化做法。因为保证电梯大小是偶数,这就诱使选手去思考下更简单的转化:如果我电梯只剩下1的空间,我挑选一个1包裹,和我把下一个2包裹拆成两个1,效果是一样的,都不影响最终答案。证明不难,也比较妙。最终结论就是,2包裹全拆成1包裹就行了,不用讨论了。

但是我想练练码力所以没转化。

#include <bits/stdc++.h> #define ll long long using namespace std; const ll N=101010; 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 T; ll n,K; struct E{ ll num,fl,cl; }a[N]; inline bool cmp(E A,E B) { return A.fl < B.fl; } ll sum[N]; ll R; void chushihua() { R = 0; } int main() { T = read(); while(T--) { chushihua(); n = read(); K = read(); for(ll i=1;i<=n;i++) { a[i].num = read(); a[i].cl = read(); a[i].fl = read(); } sort(a+1,a+n+1,cmp); for(ll i=1;i<=n;i++) { sum[i] = sum[i-1] + (a[i].cl==1); if(a[i].cl==1) R = i; } ll ans = 0; for(ll i=n;i>=1;i--) { // for(ll j=1;j<=n;j++) cout<<a[j].fl<<","<<a[j].cl<<","<<a[j].num<<" "; // cout<<"ans = "<<ans<<endl; if(a[i].cl * a[i].num >= K) ans += a[i].fl * ((a[i].cl * a[i].num) / K), a[i].num %= K / a[i].cl; if(!a[i].num) continue; ll now = K; ll j = i; ans += a[i].fl; while(1) { if(a[j].cl * a[j].num < now) now -= a[j].cl * a[j].num, a[j].num = 0; else { a[j].num -= now / a[j].cl; now %= a[j].cl; break; } j--; if(j==0) break; } if(j==0) break; R = min(j,R); if(sum[R]==0 || now==0) continue; ll l = 1, r = R, mid, res; while(l<=r) { mid = l+r >> 1; if(sum[mid]==sum[j]) res = mid, r = mid-1; else l = mid+1; } R = res; a[R].num--; if(!a[R].num) R--; } cout<<ans<<"\n"; } return 0; }

1|7M:


好经典的题意,上一次见降雨存水的题好像是一道防AK几何。

好心的出题人怕大家做不出来,温馨地给了一个“更正式地”,其实是个关键的题意转化:ans=i=1n(min(fi,gi)ai)fi,gi 分别表示前缀最大值和后缀最大值。这个转化是在枚举每一个位置上有多高的水柱。

更新 ai 时,单独更新每一个位置的 fi,gi 是比较好更新的,可是它俩的 min 值就不是很方便更新,能不能进一步转化?

我们发现,fi,gi本来就是左右的最大值,这俩已经包括了全局所有的数,那么对它们取个min,是不是跟全局次大值有关呢。

只要能想到这一点,不难转化成下式:min(fi,gi)=fi+gimax(fi,gi)=fi+gimaximaxi 表示全局最大值。ans=i=1nfi+gimaxiai

因为 ai 的修改只有增加,这就方便了我们对这三个值的维护,比如 fi 是单增的,就可以用 set 维护,相同的数值只需在 set 中记录第一个位置。

#include <bits/stdc++.h> #define ll long long using namespace std; const ll N=501010; const ll inf=0x3f3f3f3f3f3f3f3f; 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 T; ll n,Q; struct E{ ll wei,zhi; }; inline bool operator < (E A,E B) { if(A.wei!=B.wei) return A.wei < B.wei; return A.zhi > B.zhi; } set <E> s; ll a[N],b[N]; ll X[N],Y[N]; ll ans[N]; ll tot,ma,F,G; void chushihua() { tot = ma = F = G = 0; for(ll i=1;i<=Q;i++) ans[i] = 0; s.clear(); } void outing() { cout<<"G = "<<G<<endl; for(auto v : s) { cout<<v.wei<<","<<v.zhi<<" "; } cout<<endl; } int main() { T = read(); while(T--) { chushihua(); n = read(); for(ll i=1;i<=n;i++) b[i] = a[i] = read(), tot += a[i], ma = max(a[i],ma); Q = read(); for(ll i=1;i<=Q;i++) { X[i] = read(); Y[i] = read(); a[X[i]] += Y[i]; ma = max(ma,a[X[i]]); tot += Y[i]; ans[i] -= tot + ma * n; // cout<<"i = "<<i<<" tot = "<<tot<<" ma = "<<ma<<endl; } a[n+1] = a[0] = inf; for(ll i=1;i<=n;i++) a[i] = b[i]; ll now = 0; s.insert({0,0}); for(ll i=1;i<=n+1;i++) { if(a[i]>now) { now = a[i]; E lst = *(--s.end()); F += (i - lst.wei) * lst.zhi; s.insert({i,a[i]}); } } for(ll i=1;i<=Q;i++) { a[X[i]] += Y[i]; E wo = {X[i], a[X[i]]}; auto it = --s.lower_bound(wo); if((*it).zhi < a[X[i]]) { ll shang = (*it).zhi; s.insert(wo); it = ++s.find(wo); F -= shang * (it->wei - wo.wei); while(it->zhi <= wo.zhi) { auto nxt = ++it; it--; F -= (nxt->wei - it->wei) * it->zhi; s.erase(it); it = nxt; } F += (it->wei - wo.wei) * wo.zhi; } ans[i] += F; } s.clear(); for(ll i=1;i<=n;i++) a[i] = b[i]; now = 0; s.insert({-n-1,0}); for(ll i=n;i>=0;i--) { if(a[i]>now) { now = a[i]; E lst = *(--s.end()); G += (-i - lst.wei) * lst.zhi; s.insert({-i, a[i]}); } } for(ll i=1;i<=Q;i++) { a[X[i]] += Y[i]; E wo = {-X[i], a[X[i]]}; auto it = --s.lower_bound(wo); if((*it).zhi < a[X[i]]) { ll shang = (*it).zhi; s.insert(wo); it = ++s.find(wo); G -= shang * (it->wei - wo.wei); while(it->zhi <= wo.zhi) { auto nxt = ++it; it--; G -= (nxt->wei - it->wei) * it->zhi; s.erase(it); it = nxt; } G += (it->wei - wo.wei) * wo.zhi; } ans[i] += G; } for(ll i=1;i<=Q;i++) cout<<ans[i]<<"\n"; } return 0; }

1|8D:


凸优化的树上DP。

f[u][i] 表示 u 这棵子树所有分支都有 i 个黑点的最小修改量,g[u][0/1] 表示把 u 这个点改成红点/黑点的代价。

有如下转化:f[u][i]=min(g[u][1]+f[v][i1],g[u][0]+f[v][i])

i 的取值是 u 子树中最短的分支,也就是说复杂度就是所有子树中最短分支的长度和,但显然当数据是链状结构时会卡死。

喜欢拿金牌的选手们会发现,这是个可以凸优化的DP,由于 DP 转移式子是将两个序列进行 (min,+) 卷积,满足闵可夫斯基和的形式,也就是两个凸包相加融合(这个在几何题中也比较常见),两个凸序列卷完还是凸序列,g 因为只有两个值所以一定是凸的,f 通过归纳自然也能证明是凸序列。

那么凸序列有什么好处呢?我们可以用 “差分序列加首项” 来维护一个凸序列,如果凸序列存在最小值,那么差分数列是单增的。回想几何中的闵可夫斯基和,我们将两个凸包上的每一条线段合在一起按斜率进行排序,序列上的 (min,+) 卷积也是如此,将首项相加,再将两个差分数列的每一个数合在一起排序,生成新的差分数列,即为卷积后 f[u] 的序列。

回到这题的做法,我们每次求 f[u] 序列,需要先将 f[v] 直接相加,再和 g[u] 做卷积,f[v] 直接相加只需要保留最短分支的长度即可(下面的代码中用 sho[u] 表示)。

这个做法最大的好处是,我们不会被链之类的数据卡掉,当我们只有一个儿子时,直接继承它的 f[v] 数列即可。

然后是和 g[u] 做卷积,分为两种:当 u 是红点时,往差分数列加入一个1;当 u 是黑点时,首项加1,再往差分数列加入一个-1。

最终答案,也就是 f[1] 的最小值,等于首项加上差分数列最小前缀(所有负数相加)。为了避免使用平衡树维护,我们观察发现加入的数字只有1和-1,我们换个简单的写法,用三个vector来记录差分数列,一个从小到大记录负数,一个记录0,一个从大到小记录正数,加入1和-1只需在vector尾部插入即可。

思路可能比较经典,但是思考和实现难度都是妥妥的金牌题。

#include <bits/stdc++.h> #define ll long long #define QWQ cout<<"QwQ"<<endl; #define FOR() ll le=e[u].size();for(ll i=0;i<le;i++) using namespace std; const ll N=501010; 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; } struct E{ ll a0,he; vector <ll> fu; vector <ll> li; vector <ll> zh; }f[N],ling; E *F[N]; // F[u] 指针指向儿子的f数列,便于直接继承。 ll T; ll n; char s[N]; ll cl[N]; vector <ll> e[N]; ll len[N],sho[N]; ll ans[N]; ll a[N]; void chushihua() { for(ll i=1;i<=n;i++) len[i] = sho[i] = 0, e[i].clear(); for(ll i=1;i<=n;i++) f[i] = ling; } void DFS(ll u) { len[u] = 1; FOR() { ll v = e[u][i]; DFS(v); len[u] = max(len[u],len[v]+1); F[u] = F[v]; if(!sho[u] || sho[v]+1 < sho[u]) sho[u] = sho[v]+1; } } void TREE(ll u) { FOR() TREE(e[u][i]); if(e[u].size()>=2) { for(ll i=0;i<=sho[u];i++) a[i] = 0; for(ll v : e[u]) { a[0] += (*F[v]).a0; ll now = 1; for(ll w : (*F[v]).fu) { a[now] += w; now++; if(now>sho[u]) break; } if(now>sho[u]) continue; for(ll w : (*F[v]).li) { now++; if(now>sho[u]) break; } if(now>sho[u]) continue; for(ll i=(*F[v]).zh.size()-1;i>=0;i--) { ll w = (*F[v]).zh[i]; a[now] += w; now++; if(now>sho[u]) break; } } *F[u] = ling; (*F[u]).a0 = a[0]; for(ll i=1;i<=sho[u];i++) { if(a[i]<0) (*F[u]).fu.push_back(a[i]), (*F[u]).he += a[i]; else if(a[i]==0) (*F[u]).li.push_back(0); } for(ll i=sho[u];i>=1;i--) { if(a[i]>0) (*F[u]).zh.push_back(a[i]); else break; } } if(cl[u]) (*F[u]).a0++, (*F[u]).fu.push_back(-1), (*F[u]).he--; else (*F[u]).zh.push_back(1); ans[u] = (*F[u]).a0 + (*F[u]).he; } int main() { T = read(); while(T--) { chushihua(); n = read(); scanf("%s",s+1); for(ll i=1;i<=n;i++) cl[i] = s[i] - '0', F[i] = &f[i]; for(ll i=2;i<=n;i++) e[read()].push_back(i); DFS(1); TREE(1); for(ll i=1;i<=n;i++) cout<<ans[i]<<" \n"[i==n]; } return 0; }

__EOF__

本文作者枫叶晴
本文链接https://www.cnblogs.com/maple276/p/18141578.html
关于博主:菜菜菜
版权声明:呃呃呃
声援博主:呐呐呐
posted @   maple276  阅读(1680)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示