联合省选A卷题解
Day1
T1 卡牌游戏
description
有 张卡牌,卡牌正面和背面各有一个数。开始时所有都是正面向上。现在可以翻至多 张牌,要求最小化正面数的极差。
solution
容易发现答案满足单调性,可以二分答案。
假设当前二分的答案为 。那么我们考虑枚举区间的左端点 ,那么相当于要求通过翻转后正面所有数都需要落在 这个区间内。即要求所有正面数 的和 的全部翻转。只要满足如下条件就满足:
- 翻转次数不大于
- 翻转后所有数仍然在要求区间内
于是乎预处理一波前后缀反面数的最大值和最小值,就可以做到快速判断了。
data range
code
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5,inf=0x3f3f3f3f;
int n,m,a[N],b[N],tmp[N],pmn[N],pmx[N],smn[N],smx[N];
inline bool _in(int pl,int pr,int l,int r){return l<=pl&&pr<=r;}
inline bool ck(int x)
{
int l=0,r=1;
for(int i=1;i<=n+n;++i)
{
int pl=tmp[i],pr=tmp[i]+x;
while(l<=n&&a[l+1]<pl)++l;
while(r<=n&&a[r]<=pr)++r;
if(pmn[l]<pl)break;
if(!_in(pmn[l],pmx[l],pl,pr))continue;
if(!_in(smn[r],smx[r],pl,pr))continue;
if(l+n-r+1>m)continue;
return true;
}
return false;
}
int main()
{
scan(n),scan(m);
for(int i=1;i<=n;++i)scan(a[i]),tmp[i]=a[i];
for(int i=1;i<=n;++i)scan(b[i]),tmp[i+n]=b[i];
pmn[0]=inf,pmx[0]=0;
for(int i=1;i<=n;++i)
pmn[i]=min(pmn[i-1],b[i]),pmx[i]=max(pmx[i-1],b[i]);
smn[n+1]=inf,smx[n+1]=0;
for(int i=n;i>=1;--i)
smn[i]=min(smn[i+1],b[i]),smx[i]=max(smx[i+1],b[i]);
sort(tmp+1,tmp+n+n+1);
int l=0,r=a[n]-a[1];
while(l+1<r)
{
int mid=(l+r)>>1;
ck(mid)?r=mid:l=mid;
}
printf("%d\n",r);
return 0;
}
T2 矩阵游戏
description
给出 的矩阵 ,求 的矩阵 ,满足
若无解,输出
solution
先不考虑所有数均在 的范围内这一要求,那么题目变得十分简单:直接钦定 这些位置为 ,然后根据 依次推即可。
考虑通过一些调整使得其满足条件。注意到对某一行/列依次 仍然满足矩阵 的条件。于是我们设每行的变化分别为 ,每列的变化分别为 。那么变化矩阵为
对于位置,我们不妨设这个变化矩阵的值为 ,那么我们需要满足
这个东西十分差分约束,但是 这种柿子化起来不太方便。于是我们可以将行数为奇数的 取反,列数为偶数的 取反,那么就有
这样一来差分约束就十分方便了。注意此题直接 可能过不了,需要加一些小优化。
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=305,V=1e6;
int n,m;
int tot,fi[N+N],ne[N*N*2],to[N*N*2],cnt[N+N];
bool inq[N+N];ll d[N+N],b[N][N],a[N][N],w[N*N*2];
inline void add(int x,int y,ll s)
{
ne[++tot]=fi[x],fi[x]=tot,to[tot]=y,w[tot]=s;
}
inline bool spfa()
{
deque<int>q;
for(int i=1;i<=n+m;++i)
d[i]=0,q.push_back(i),inq[i]=1,cnt[i]=1;
while(!q.empty())
{
int u=q.front();q.pop_front();inq[u]=0;
for(int i=fi[u];i;i=ne[i])
{
int v=to[i];
if(d[v]<d[u]+w[i])
{
d[v]=d[u]+w[i];
if(!inq[v])
{
inq[v]=1;
if(d[v]>d[q.front()])q.push_front(v);
else q.push_back(v);
if(d[q.front()]<d[q.back()])swap(q.front(),q.back());
if(++cnt[v]>n+m)return false;
}
}
}
}
return true;
}
inline void clear(){tot=0;fill(fi+1,fi+n+m+1,0);}
int main()
{
int T;scan(T);
while(T--)
{
scan(n),scan(m);clear();
for(int i=2;i<=n;++i)
for(int j=2;j<=m;++j)
scan(b[i][j]);
for(int i=1;i<=n;++i)a[i][1]=0;
for(int i=2;i<=m;++i)a[1][i]=0;
for(int i=2;i<=n;++i)
for(int j=2;j<=m;++j)
a[i][j]=b[i][j]-a[i-1][j-1]-a[i][j-1]-a[i-1][j];
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if((i+j)&1)add(i,j+n,-a[i][j]),add(j+n,i,a[i][j]-V);
else add(j+n,i,-a[i][j]),add(i,j+n,a[i][j]-V);
if(!spfa()){pc('N');pc('O');pc('\n');;continue;}
pc('Y');pc('E');pc('S');pc('\n');
for(int i=1;i<=n;++i,pc('\n'))
for(int j=1;j<=m;++j)
{
ll tmp=((i+j)&1)?(-d[i]+d[j+n]):(d[i]-d[j+n]);
putint(a[i][j]+tmp,'\n');
}
}flush();
return 0;
}
T3 图函数
description
对于有向图 和其中一点 定义函数 :
定义变量 ,初值为 。
按顺序依次枚举 ,记为 ,倘若存在路径 ,那么 ,同时删除点 以及所有和它相连的边。
定义 , 表示 删除前 条边后得到的图,求
solution
注意到我们并不关心对于每个点的 是多少,我们更关心的是直接求出
一对点 对答案有贡献当且仅当原图上存在路径 ,且这两个路径所经过的所有节点中所有编号的 。
考虑证明。假设其上存在点 其编号在区间 ,那么
- 如果先前 做出过贡献,那么 会被删去,显然不可行。
- 否则,那么路径 至少有一条不可行。但是 均可行,这与之矛盾,不可行。
于是我们可以使用 ,用类似于求最短路的方法从大到小枚举中间点,就可以求出每对点是否可以作出贡献,最后统计答案即可。
现在回到原问题。根据套路,肯定是从后往前来想,转删边为加边。考虑到如果 带来在某个时间的贡献,那么对于整个后缀都会带来贡献。
令 表示最小的时间使得存在路径 且路径上所有点的编号都 。转移的话类似于上述的做法。
对于 这一区间的所有值都会产生贡献,于是差分一波,最后求前缀和就可以求出答案了。
code
#include<bits/stdc++.h>
const int N=1005,M=2e5+5;
int n,m,ans[M],f[N][N],*t1;
inline void Max(int &a,int b){(b>a)&&(a=b);}
int main()
{
scan(n),scan(m);
for(int i=1,u,v;i<=m;++i)
scan(u),scan(v),f[u][v]=i;
for(int i=1;i<=n;++i)f[i][i]=m+1;
for(int k=n;k;--k)
{
t1=f[k];
for(int i=1;i<=n;++i)
{
const int mx=(i<=k)?n:k,now=f[i][k];
if(now<=0)continue;
for(int j=1;j<=mx;++j)
Max(f[i][j],min(now,t1[j]));
}
}
for(int i=1;i<=n;++i)for(int j=i;j<=n;++j)++ans[min(f[i][j],f[j][i])];
for(int i=m;i;--i)ans[i]+=ans[i+1];
for(int i=1;i<=m+1;++i)putint(ans[i],' ');flush();
return 0;
}
Day 2
T1 宝石
description
给定一棵树, 个节点,每个点有一个宝石,类型为 ,约定 。你有一个收集器,可以收集至多 个宝石,并且收集顺序必须为 ,其中 互不相同。先有 次询问,每次询问一条有向路径 ,求依次最多可以收集多少宝石。
solution
令 ,那么问题可以拆为 两个部分。
考虑第一部分。
注意到 互不相同,因此我们可以记下 这个数组表示 中某个字符的在 中的后继。然后对于树上每一个节点,如果它在 中出现,那么向它的祖先中距离它最近的后继连边。这样匹配的时候只用找到 祖先中距离 最近的 ,然后从 开始往上跳直到 的位置。这一部分显然可以倍增优化,复杂度降为
考虑第二部分。
我们类似地维护 数组表示前驱。但是现在的问题在于我们并不知道该从那个字符开始。注意到这个答案满足单调性,即如果 可行,那么 一定可行。于是我们可以二分答案,而后从该字符向上跳直到 ,最后看看和左边接上了没有。
现在的问题在于如何对于一个节点快速求出在其祖先中离它最近的某个字符。其实可以将所有询问离线下来, 一遍就可以求得所有答案。即使要求强制在线,也可以用主席树维护可持久化数组,复杂度仍然不变。
time complexity
code
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,M=5e4+5,H=18;
int tot,fi[N],ne[N<<1],to[N<<1],nxt[M],pre[M],fa[N][20],pa[N][20];
inline void add(int x,int y)
{
ne[++tot]=fi[x],fi[x]=tot,to[tot]=y;
}
int jp[N][20],dep[N],tin[N],tout[N],tim,n,m,c,p[M],w[N];
inline bool isac(int x,int y){return tin[x]<=tin[y]&&tout[y]<=tout[x];}
void dfs(int u,int f)
{
tin[u]=++tim;
jp[u][0]=f;dep[u]=dep[f]+1;
for(int i=1;(1<<i)<=dep[u];++i)
jp[u][i]=jp[jp[u][i-1]][i-1];
for(int i=fi[u];i;i=ne[i])
{
int v=to[i];
if(v!=f)dfs(v,u);
}tout[u]=++tim;
}
inline int lca(int x,int y)
{
if(dep[x]>dep[y])swap(x,y);
if(isac(x,y))return x;
for(int i=H;~i;--i)
if(!isac(jp[x][i],y))x=jp[x][i];
return jp[x][0];
}
struct node{int to,id;};
vector<node>t1[N],t2[N];int ans[N],pos[M];
void solve1(int u,int f)
{
int oq=0;oq=pos[w[u]],pos[w[u]]=u;
if(nxt[w[u]])
{
int now=pos[nxt[w[u]]];
if(now)fa[u][0]=w[now]==w[u]?fa[now][0]:now;
for(int i=1;;++i)
{
fa[u][i]=fa[fa[u][i-1]][i-1];
if(!fa[u][i])break;
}
}
for(node o:t1[u])
{
int now=pos[p[1]],lim=dep[o.to];
if(dep[now]<lim)continue;
ans[o.id]=1;
for(int i=H;~i;--i)
if(dep[fa[now][i]]>=dep[o.to])
now=fa[now][i],ans[o.id]+=(1<<i);
}
for(int i=fi[u];i;i=ne[i])if(to[i]!=f)solve1(to[i],u);
pos[w[u]]=oq;
}
inline bool ck(int l,int x,int lim)
{
int now=pos[p[x]],t=x;
if(dep[now]<=lim)return false;--t;
for(int i=H;~i;--i)
if(dep[pa[now][i]]>lim)
now=pa[now][i],t-=(1<<i);
return l>=t;
}
void solve2(int u,int f)
{
int oq=0;oq=pos[w[u]],pos[w[u]]=u;
if(pre[w[u]])
{
int now=pos[pre[w[u]]];
if(now)pa[u][0]=w[now]==w[u]?pa[now][0]:now;
for(int i=1;;++i)
{
pa[u][i]=pa[pa[u][i-1]][i-1];
if(!pa[u][i])break;
}
}
for(node o:t2[u])
{
int lim=dep[o.to];
int l=ans[o.id],r=c+1;
while(l+1<r)
{
int mid=(l+r)>>1;
ck(ans[o.id],mid,lim)?l=mid:r=mid;
}ans[o.id]=l;
}
for(int i=fi[u];i;i=ne[i])if(to[i]!=f)solve2(to[i],u);
pos[w[u]]=oq;
}
int main()
{
scan(n),scan(m),scan(c);
for(int i=1;i<=c;++i)scan(p[i]);
for(int i=1;i<c;++i)nxt[p[i]]=p[i+1];
for(int i=2;i<=c;++i)pre[p[i]]=p[i-1];
for(int i=1;i<=n;++i)scan(w[i]);
for(int i=1,u,v;i<n;++i)
scan(u),scan(v),add(u,v),add(v,u);
dfs(1,0);tin[0]=0,tout[0]=++tim;
int q;scan(q);
for(int i=1,u,v;i<=q;++i)
{
scan(u),scan(v);
int p=lca(u,v);
t1[u].push_back({p,i});
t2[v].push_back({p,i});
}
solve1(1,0);solve2(1,0);
for(int i=1;i<=q;++i)putint(ans[i],'\n');flush();
return 0;
}
T2 滚榜
description
给出数组 和 ,初始 ,现在以不降的顺序给出,要求 。
定义 比 优秀当且仅当 或 且
现在已知每次公布了某个 的 后, 都成为了最优秀的。
求有可能的排列个数。
solution
首先有种非常暴力的做法:暴力枚举所有的排列,然后贪心地判断是否可行,这样可以拿到 分的高分!
这样过于暴力了。考虑令 表示当前已经考虑了的数的集合为 ,上一个公布的为 , 的分数为 ,且现在所有 的和为 时有多少种可能,转移的话和贪心一样。
但是这样得的分甚至连暴力都比不过,考虑优化状态。其实 是不必要的,因为 的存在是为了给下一次的分数提供下限,而其实每次给某个 增加 时可以为所有未公布成绩的队伍全部增加 (之所以能这样做是因为保证了 一定是单调不降)。这样一来每次只用比较 的大小即可转移。而增加 也不用直接加,因为相对大小并不会改变,只需要在剩余题数上减去若干个 即可。这其实和差分本质上式相同的。
code
#include<bits/stdc++.h>
using namespace std;
const int N=13,M=505;
typedef long long ll;
int n,a[N],pos,m;
ll f[1<<N][N][M],ans;
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;++i)scanf("%d",a+i);
for(int i=0;i<n;++i)if(a[i]>a[pos])pos=i;
f[0][pos][m]=1;int st=1<<n;
for(int i=1;i<st;++i)
{
int num=n-__builtin_popcount(i);
for(int j=0;j<n;++j)
{
if(!((i>>j)&1))continue;
int s=i^(1<<j);
for(int k=0;k<n;++k)
if(((s>>k)&1)||(!s&&k==pos))
{
int now=a[k]-a[j];if(k<j)++now;
now=max(now,0);
int tmp=(num+1)*now;
for(int t=tmp;t<=m;++t)f[i][j][t-tmp]+=f[s][k][t];
}
if(i==st-1)for(int k=0;k<=m;++k)ans+=f[i][j][k];
}
}
printf("%lld\n",ans);
return 0;
}
T3 支配
description
给定一个有向图,保证从节点 出发可以到达任意节点。定义节点 的支配集为一点集,任意一条从 到 的路径都会经过这一点集中的所有点。现在每次询问给出 ,求连边 后有多少个节点的支配集发生变化。
solution
注意到支配关系具有传递性,即如果 支配 , 支配 ,那么 支配 。同时支配关系不会存在环,因此我们可以大胆猜想支配关系可以形成一棵树。对于树上节点 ,它所有祖先都可以支配它。(这样的证明纯属口胡,严谨的可以去看其他题解)
考虑如何构造出这棵支配树。我们可以通过枚举删除每一个点,然后判断哪些点不可达,从而求出每个点的支配集,同时也可以求出每个点 支配那些点,即这个集合大小为。然后将所有点按照 从大到小排序,由这个点向每个它支配的点连边,作为它的直接儿子。可以证明这样做是正确的。如果想了解 的做法可以出门左转。
现在考虑询问。给出了一条 的边,我们考虑在必须经过这条边的情况下能使哪些点的支配集变小,或者说可以不用经过这些点的某些祖先。注意到这棵支配树的本质是从 到所有节点 的所有路径的压缩表达,即要到达 ,一定是从 开始到 这条路径上的点依次经过。不妨设 ,且假设加入 的边后 的支配集变小,那么原图上一定存在一条路径形如 。注意到这样必然会经过 这些路径上的所有点,因此可能不被经过的就只有 这些点了。如果在 的过程中我们到达了支配树上 的某一祖先节点,那么要到达 必然只能沿着这条链走到 ,否则必然至少一个点可以不被经过,这样这个点就不是 的支配点,与定义不符,矛盾。
更进一步,我们发现只要在 的过程中不经过 及其祖先以及它们的所有直接儿子和 的支配集减小是充分必要条件。
- 充分性:既然存在这样一条路径,那么 这条路径上一定有节点没有被经过,支配集减小。
- 必要性:必然存在一条经过 的路径且 上有节点没有被经过。由先前的论证,这条路径一定不会经过 (否则直接沿着走下来所有点都要经过)和其直接儿子(相当于和 拼在一起,没有节点不被经过)。
于是对于每个询问,直接 即可。
#include<bits/stdc++.h>
using namespace std;
const int N=3005;
vector<int>e[N];
bool ct[N][N];int vis[N],pd[N];
int n,m,q,cnt[N],p[N],dep[N],fa[N];
int tot,fi[N],ne[N<<1],to[N<<1];
inline bool cmp(const int&x,const int&y){return cnt[x]>cnt[y];}
inline void add(int x,int y)
{
ne[++tot]=fi[x],fi[x]=tot,to[tot]=y;
}
void go(int u,int ban)
{
if(u==ban||vis[u])return;
vis[u]=1;
for(int i=fi[u];i;i=ne[i])go(to[i],ban);
}
inline void pre()
{
for(int i=1;i<=n;++i)
{
fill(vis+1,vis+n+1,0);go(1,i);
for(int j=1;j<=n;++j)
if(!vis[j])ct[j][i]=1,++cnt[i];
}
}
void dfs(int u)
{
for(int v:e[u])
dep[v]=dep[u]+1,dfs(v);
}
inline int lca(int x,int y)
{
if(dep[x]<dep[y])swap(x,y);
while(dep[fa[x]]>=dep[y])x=fa[x];
if(x==y)return x;
while(fa[x]!=fa[y])x=fa[x],y=fa[y];
return fa[x];
}
inline int bfs(int now,int id)
{
int ans=0;
queue<int>q;if(vis[now]!=id)q.push(now),pd[now]=id,++ans;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=fi[u];i;i=ne[i])
{
int v=to[i];
if(vis[v]!=id&&pd[v]!=id)
q.push(v),pd[v]=id,++ans;
}
}
return ans;
}
int main()
{
scan(n),scan(m),scan(q);
for(int i=1,u,v;i<=m;++i)
scan(u),scan(v),add(u,v);
pre();
for(int i=1;i<=n;++i)p[i]=i;
sort(p+1,p+n+1,cmp);
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
if(p[i]!=j&&ct[j][p[i]])
fa[j]=p[i];
for(int i=1;i<=n;++i)if(fa[i])e[fa[i]].push_back(i);
dep[1]=1;dfs(1);
for(int i=1;i<=q;++i)
{
int s,t;scan(s),scan(t);
int p=lca(s,t);
while(p)
{
for(int v:e[p])vis[v]=i;
vis[p]=i;p=fa[p];
}
putint(bfs(t,i),'\n');
}flush();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现