ICPC2023香港站题解(A D E H I J)

本场金牌为超低罚时六题,稳拿金牌需要做出第七题。

但是我只会六题,这里是前六题的题解。

1|0ICPC2023香港站


1|1J:


签到但不是完全签到,需要讲。

首先每个位置只会走一次,所以让 ai 加一的操作只会在第一次到达某个位置时连续施行。

ai 加一再跳转需要花费一个时间,让 ai 加二再跳转需要花费两个时间,你可以理解成先走到 i+ai 的位置,再花费 1 的时间往后走一个位置,也可以花费 2 的时间往后走两个位置。所以我们把每一个位置往下一个位置连花费为1的边就好了。

但这个连边是需要一个前提的,我们得能够先走到 i 的位置,这些向后连的边才有意义。比如终点是1,但是 a0=2,你是不能一步到达的。

解决方案很简单,不要把0当初始点,而是把 a0 当初始点,因为走到终点是至少需要跳这一步的。

#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; }

1|2A:


一道很像网络流的矩阵选数题。题面中的 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; }

1|3I:


当你的重置能力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; }

1|4D:


每个点入度上限为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; }

1|5H:


妙妙树上DP,我写的这个是一个很奇怪的做法。

首先要发现一点:虽然车是按从小到大一个一个寻找空位的,但我们不需要关心它们的具体顺序,只要是一个合法的方案,我们调换任意两辆车的顺序,依旧是合法的。

所以我们dp时就不需要考虑车的编号了,只需要考虑每个点上有几辆车。

最后的答案如何统计,假如第 i 个点停了 bi 辆车,那么这样的方案对应到车辆编号的情况数就有 n!b1!b2!...bn! 种,也就是超排列。我们不能让最后的方案数直接乘以 n! 就是因为有些车是在同一个点的,直接乘以 n! 会重复计算这一部分。我们 dp 出来的方案数不好统计究竟有多少车在同一个点,但我们发现可以把 bi! 分母的这部分先计入我们的dp,也就是说,只要选了一个大小为 bi 的点(bi 辆车在这个点),我们就让 dp 结果除以 bi,最后再乘上一个 n! 就是最终答案了。

dp 方程怎么设,怎么转移,需要观察题目的性质。

因为题目中的过程所有车都是根向走的,我们将时间倒流,每个点上有一辆车,所有车往叶向走回到初始时刻,你会发现一个性质,每一棵子树中车的数量一定不小于子树大小,而且多出来的部分也不会超过这棵子树到根的距离。题目所给的 “随机数据” 的性质就在这里体现了,每个点到根的距离期望值不会超过log。

我们设 f[u][i] 表示 u 这棵子树中车比点多了 i 个的方案数,儿子们多出来的点数加起来就是父亲多出来的点数,所以这是一个累加求和的背包问题。合并完之后我们要选择 u 这个父亲结点上车的数量,这时我们要让答案除以 bu,具体含义就是上一段所讲的内容。

#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; }

1|6E:


这题我们捏了个很有趣的东西哈哈,我们称之为 “左偏笛卡尔树”

我们的思路是酱紫的:

首先看字典序最小的拓扑排序,我们找到最大的那个数字,连一条边让它指向右边的整体,表示原图中肯定是先访问了这个点才能访问右边的那些点,否则这个数字就不会出现在这里。

而它左边的哪些数字呢,我们把它们视作并列的兄弟关系,因为即使它们之间相互不连边,在最小拓扑排序中数大的点依旧是后访问。

因此我们可以递归地构建一棵树:找到区间中最大的数,左边的整体成为它的兄弟,右边的整体成为它的儿子。

左右区间各找最大点成为儿子,这样构造的树是标准的笛卡尔树,而我们这个是右边成为儿子,左边成为兄弟,我们形象地称之为 “左偏笛卡尔树”

然后是最大的拓扑排序,原理是一样的,我们继续构造一棵树,把两棵树的所有边加在一起,就是答案的图。(这里要注意第二棵树的边往第一棵树里加时,不允许出现右边的点连向左边这种情况,可以用第一棵树的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; }

__EOF__

本文作者枫叶晴
本文链接https://www.cnblogs.com/maple276/p/18144976.html
关于博主:菜菜菜
版权声明:呃呃呃
声援博主:呐呐呐
posted @   maple276  阅读(284)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示