2025.7.3 题解
100+0+0+10=110,rk7。
A 返乡
神秘诈骗题。容易想到当且仅当三门科目的成绩和为 \(\dfrac{3n}{2}\)(上、下取整皆可)时,方案数最多。此时暴力枚举前两门的分数,可以做到时间复杂度 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;
struct syz{int a,b,c;};
int n,sum,ans;vector<syz>g;
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n,sum=n*3/2;
for(int i=0;i<=n;i++)
for(int j=0;i+j<=sum&&j<=n;j++)
if(sum-i-j<=n) g.push_back({i,j,sum-i-j});
cout<<g.size()<<"\n";
for(auto x:g) cout<<x.a<<" "<<x.b<<" "<<x.c<<"\n";
return 0;
}
B 连接
神人题,写正解的人不到写其他解的人的零头。
容易发现目标区间必有一端与一根钢管的一端重合,且若不是两端都重合,那么所截钢管质量必为 \(L/R\)。
我们可以二分出每根钢管所对应质量为 \(L,R\) 的位置,然后算出所有一端补充和的情况。
设 \(sm_i,sl_i\) 表示钢管质量和长度的前缀和,那么我们可以将几根钢管的平均密度用 \(\dfrac{sm_i-sm_j}{sl_i-sl_j}\) 表示。显然若 \(i>j\),则 \(sm_i>sm_j,sl_i>sl_j\)。容易发现我们可以将这个分数看作斜率,那么若有三个点 \(i<j<k\),且 \(j\) 在 \(i,k\) 连线上侧,则 \(j\) 永远不可能对答案产生贡献。根据这一结论,我们可以维护下凸包,并在下凸包上二分,即能找到答案。由于只有一段区间内的钢管可以用于统计答案,所以需要用线段树维护。
时间复杂度 \(O(n\log^2n)\),但跑的比所谓“时间复杂度接近单 \(\log\)”的正解快到不知道哪里去了。
#include<bits/stdc++.h>
#define int long long
#define fxs(x) fixed<<setprecision(x)
using namespace std;
const int N=3e5+5;
double ans,sl[N],sm[N];
int n,lm,rm,len[N],p[N],tp;
vector<int>sk[N*4];int st[N];
double jl(int x,int y){
return (sm[y]-sm[x])/(sl[y]-sl[x]);
}int check(int x,int y,int z){
return jl(x-1,y-1)>=jl(y-1,z-1);
}void build(int x,int l,int r){
tp=0;for(int i=l;i<=r;st[++tp]=i++)
while(tp>1&&check(st[tp-1],st[tp],i)) tp--;
for(int i=1;i<=tp;i++) sk[x].push_back(st[i]);
if(l==r) return;int mid=(l+r)/2;
build(x*2,l,mid),build(x*2+1,mid+1,r);
}double maxn(int x,int l,int r,int L,int R,int id){
if(L>R) return 0;
if(L<=l&&r<=R){
int lc=1,rc=sk[x].size()-1,re=sk[x][0];
while(lc<=rc){
int mid=(lc+rc)/2;
if(check(sk[x][mid-1],id+1,sk[x][mid])) rc=mid-1;
else lc=mid+1,re=sk[x][mid];
}return jl(re-1,id);
}int mid=(l+r)/2;double re=0;
if(L<=mid) re=maxn(x*2,l,mid,L,R,id);
if(R>mid) re=max(re,maxn(x*2+1,mid+1,r,L,R,id));
return re;
}signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>lm>>rm;
for(int i=1;i<=n;i++) cin>>len[i];
for(int i=1;i<=n;i++) cin>>p[i];
for(int i=1;i<=n;i++)
sl[i]=sl[i-1]+len[i],sm[i]=sm[i-1]+len[i]*p[i];
build(1,1,n);
for(int i=1,l,r;i<=n;i++){
if(sm[i]<lm) continue;
r=upper_bound(sm+1,sm+i+1,sm[i]-lm)-sm;
if(sm[i]<=rm) l=1;
else l=lower_bound(sm+1,sm+i+1,sm[i]-rm)-sm+1;
ans=max(ans,maxn(1,1,n,l,r,i));
ans=max(ans,lm/(sl[i]-sl[r]+(lm-sm[i]+sm[r])/p[r]));
if(l>1) ans=max(ans,rm/(sl[i]-sl[l-1]+(rm-sl[i]+sl[l-1])/p[l-1]));
}for(int i=1;i<=n/2;i++)
swap(len[i],len[n-i+1]),swap(p[i],p[n-i+1]);
for(int i=1;i<=n;i++)
sl[i]=sl[i-1]+len[i],sm[i]=sm[i-1]+len[i]*p[i];
for(int i=1,l,r;i<=n;i++){
if(sm[i]<lm) continue;
r=upper_bound(sm+1,sm+i+1,sm[i]-lm)-sm;
if(sm[i]<=rm) l=1;
else l=lower_bound(sm+1,sm+i+1,sm[i]-rm)-sm+1;
ans=max(ans,lm/(sl[i]-sl[r]+(lm-sm[i]+sm[r])/p[r]));
if(l>1) ans=max(ans,rm/(sl[i]-sl[l-1]+(rm-sl[i]+sl[l-1])/p[l-1]));
}cout<<fxs(10)<<ans;
return 0;
}
C 习惯孤独
《谈 \(UKE-Automation\) 使用 \(bfs\) 维护换根 dp 这件事》
显然,我们可以算出我们割掉的每个联通块的大小。
考虑 \(k\) 很小,于是设 \(f_{i,s}\) 表示 \(i\) 子树内割掉的联通块状态为 \(s\) 时,方案的数量。为了方便统计答案,再设 \(d_{i,s}\) 统计 \(i\) 本身没有被割掉的情况。我们定义 \(id(\alpha)\) 表示 \(\alpha\) 表示的是第几次切割,\(sz(s)\) 表示经过状态为 \(s\) 的切割,切掉的联通块大小。那么有:
但这是不够的,因为我们可能还在子树外切割了联通块,所以我们需要伟大的换根 dp。
我们设 \(g_{i,s}\) 表示 \(i\) 及 \(i\) 子树外割掉的联通块状态为 \(s\) 时,方案的数量。为了方便统计答案,再设 \(h_{i,s}\) 统计 \(i\) 本身没有被割掉的情况。当然,这些还不够,我们还需要设 \(pre_{i,j,s}\) 表示 \(i\) 的前 \(j\) 个儿子合并到一起,状态为 \(s\) 时的方案数,\(suf_{i,j,s}\) 则是后 \(j\) 个儿子,方程的基础逻辑和上面两个方程相似。
最后对于每个点 \(i\),把 \(d_i,h_i\) 合并到一起,并将合并后的数组中第 \(2^k-1\) 项全部加起来就是答案。时间复杂度 \(O(n3^k)\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5005,K=64,p=998244353;
int n,k,f[N][K],g[N][K],sf[N][K],sz[N],ans;
vector<int>ve[N];int d[N][K],h[N][K],a[6];
void merge(int *f,int *g){
static int tmp[K];
memset(tmp,0,sizeof(tmp));
for(int i=0;i<(1<<k);i++){
for(int j=i;j;j=(j-1)&i)
tmp[i]=(tmp[i]+f[j]*g[i^j])%p;
tmp[i]=(tmp[i]+f[0]*g[i])%p;
}for(int i=0;i<(1<<k);i++) f[i]=tmp[i];
}void add(int *f,int *g,int s){
static int tmp[K];
memset(tmp,0,sizeof(tmp));
for(int i=0;i<(1<<k);i++){
int sum=s,lst=-1;
for(int j=0;j<k;j++)
if((i>>j)&1) sum-=a[j],lst=j;
for(int j=lst+1;j<k;j++)
if(sum==a[j]) tmp[i|(1<<j)]=(tmp[i|(1<<j)]+f[i])%p;
}for(int i=0;i<(1<<k);i++) f[i]=(f[i]+(g[i]=tmp[i]))%p;
}void dfs1(int x,int fa){
f[x][0]=1,sz[x]=1;
for(auto y:ve[x]) if(y!=fa)
dfs1(y,x),merge(f[x],f[y]),sz[x]+=sz[y];
static int tmp[K];
memcpy(d[x],f[x],sizeof(d[x]));
add(f[x],tmp,sz[x]);
}void dfs2(int x,int fa){
for(int i=1;i<=ve[x].size()+1;i++)
memset(sf[i],0,sizeof(sf[i]));
sf[ve[x].size()+1][0]=1;
for(int i=ve[x].size();i;i--){
int y=ve[x][i-1];
memcpy(sf[i],sf[i+1],sizeof(sf[i]));
if(y!=fa) merge(sf[i],f[y]);
}static int pr[K];
memset(pr,0,sizeof(pr)),pr[0]=1;
for(int i=1;i<=ve[x].size();i++){
int y=ve[x][i-1];if(y==fa) continue;
memcpy(g[y],pr,sizeof(g[y]));
merge(g[y],sf[i+1]),merge(g[y],g[x]);
add(g[y],h[y],n-sz[y]),merge(pr,f[y]);
}for(auto y:ve[x]) if(y!=fa) dfs2(y,x);
}signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0),cin>>n;
for(int i=1,u,v;i<n;i++)
cin>>u>>v,ve[u].push_back(v),ve[v].push_back(u);
cin>>k;for(int i=0;i<k;i++) cin>>a[i];
for(int i=k-1;i;i--) a[i]=a[i-1]-a[i];
a[0]=n-a[0],dfs1(1,0),g[1][0]=h[1][0]=1,dfs2(1,0);
for(int i=1;i<=n;i++)
merge(d[i],h[i]),ans+=d[i][(1<<k)-1];
return cout<<ans%p,0;
}
D 车站
震惊!某刘姓男子深耕朱刘领域 4 小时终 AC,但讲题时连翻 3 车!
看到 \(k=1\),想到最小树形图。朴素朱刘是 \(O(nm)\) 的,但由于朱刘算法本质只需要完成三个操作:
- 找到最小出边。
- 给一个点的所有出边同加一个值。
- 合并两个边集。
第一个可以用小根堆,结合第三个想到可并堆或启发式合并,加上第二个,锁定左偏树或启发式合并+标记,时间复杂度分别为 \(O(n\log m+m)\) 和 \(O(n\log^2m+m)\)。本篇题解使用后者。
对于本题,我们可以暴力枚举所有可能的终点情况,时间复杂度为 \(O(\binom{n}{k}(n\log^2m+m))\)。但这样想是没有前途的。
为方便叙述,下面将朱刘算法结束后展开的环点并存的图中,每个简单环和点称为大点。
我们学习肯尼迪精神,脑洞大开,计算每条边在朱刘中可能的贡献。我们会发现,假如一条边从大点 \(u\) 连向大点 \(v\),权值为 \(w\),那么当且仅当大点 \(u\) 中没有终点站时,这条边会产生贡献,即 \(ans=\sum\dfrac{\binom{n-sz_u}k}{\binom nk}w\)。原因是我们要求的最小树形图实际上是一个内向树,所以,每个大点只有一条出边。
虽然根据同学们的奋斗结果显示,本题并没有无解的数据,但是我们仍然来判断一下。考虑最有可能无法到达终点站的大点一定是没有出边的大点,同时没有出边的大点中,\(sz\) 越小,对 \(k\) 的限制越强。设最小的无出边大点为 \(x\),则当且仅当 \(k\le n-sz_x\) 时,原问题无解。
时间复杂度 \(O(n\log^2m+m)\) 或 \(O(n\log m+m)\)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=9e5+5,M=310005,p=998244353;
int qpow(int x,int y){
int re=1;
while(y){
if(y&1) re=re*x%p;
x=x*x%p,y>>=1;
}return re;
}struct ed{int to,cs;}out[M];
bool operator<(ed x,ed y){return x.cs>y.cs;}
struct prque{
priority_queue<ed>q;int lz=0;
int size(){return q.size();}void add(int x){lz+=x;}
ed top(){ed c=q.top();return {c.to,c.cs+lz};}
void push(ed x){q.push({x.to,x.cs-lz});}
void pop(){q.pop();}
}q[M];int n,m,k,tot,ans,prt=1e9;
void merge(int x,int y){
if(q[x].size()<q[y].size()) swap(q[x],q[y]);
while(q[y].size()) q[x].push(q[y].top()),q[y].pop();
}int jc[N],inv[N],fa[M];
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}struct idx{
int sz[M],fa[M];
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
}id;vector<int>r;
vector<vector<int> >h;
void init(int n){
jc[0]=inv[0]=1;
for(int i=1;i<=n;i++)
jc[i]=jc[i-1]*i%p,id.fa[i]=fa[i]=i;
inv[n]=qpow(jc[n],p-2);
for(int i=n-1;i;i--)
inv[i]=inv[i+1]*(i+1)%p;
}int C(int x,int y){
if(x<y||y<0) return 0;
return jc[x]*inv[y]%p*inv[x-y]%p;
}signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>k,tot=n,init(2*n+k+1);
for(int i=1,u,v,w;i<=m;i++)
cin>>u>>v>>w,q[u].push({v,w});
for(int i=1;i<=n;i++){
id.sz[i]=1;if(q[i].size()) out[i]=q[i].top();
}for(int i=1;i<=n;i++){
if(!out[i].to) prt=1;
else r.push_back(i);
}if(prt<1e9) cout<<(k==n?1:-1),exit(0);
while(1){
if(!r.size()) break;
for(auto x:r){
int y=out[x].to,cs=out[x].cs;
ans=(ans+C(n-id.sz[id.find(x)],k)*cs)%p;
if(find(x)==find(y)){
vector<int>ah;int ht=id.find(y);ah.push_back(x);
while(ht!=x) ah.push_back(ht),ht=id.find(out[ht].to);
h.push_back(ah);
}else fa[find(x)]=find(y);
}r.clear();
for(vector<int>ah:h){
tot++;for(auto x:ah) id.sz[tot]+=id.sz[x],fa[find(x)]=id.fa[x]=tot;
for(auto x:ah){
while(q[x].size()&&id.find(q[x].top().to)==id.find(tot)) q[x].pop();
q[x].add(-out[x].cs),merge(tot,x);
}if(q[tot].size()) out[tot]=q[tot].top();
if(!out[tot].to) prt=min(prt,id.sz[tot]);
else r.push_back(tot);
}h.clear();
}if(k<=n-prt) cout<<-1;
else cout<<ans*qpow(C(n,k),p-2)%p;
return 0;
}

浙公网安备 33010602011771号