【考试题解】NOIP2024(欢乐)加赛3

A. Sakurako and Water

题目内容

给你一个 n×n 的带权矩阵,每次操作选中一个正方形区域并把从左上角到右下角的对角线(主对角线)上的每个格子的权值 +1,求使整个矩阵所有点权值非负的最小操作次数

思路

有一个显然的性质:对于矩阵中任意一条主对角线进行操作,如果它还能再向左上或右下扩展,那么一定不如扩展到不能再扩展更优。这样,整个矩形只剩下了 2n1 条有用的主对角线。对一条主对角线的最小操作次数就是在它上面的点的最小权值的绝对值。直接左右反转整个矩阵,然后对于每个点,找到它属于哪个从左下到右上的对角线是好做的。最后枚举对角线统计答案。

代码

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define inf 0x3f3f3f3f
int a,b,c[505][505],mx[1001];
long long ans;
int main()
{
scanf("%d",&a);
while(a--)
{
scanf("%d",&b);
for(ri i=1;i<=b;i++)
{
for(ri j=b;j>=1;j--)
{
scanf("%d",&c[i][j]);
}
}
memset(mx,0,sizeof(mx));
for(ri i=1;i<=b;i++)
{
for(ri j=1;j<=b;j++)
{
mx[i+j-1]=min(mx[i+j-1],c[i][j]);
}
}
ans=0;
for(ri i=1;i<=b<<1;i++)
{
ans-=mx[i];
}
printf("%lld\n",ans);
}
return 0;
}

B. Binomial Coefficients, Kind Of

题目内容

给你一个错误的递推求组合数的代码:

for(int n = 0; n < N; n++) {
C[n][0] = 1;
C[n][n] = 1;
for(int k = 1; k < n; k++)
C[n][k] = C[n][k-1] + C[n-1][k-1];
}

t 次询问,每次给定 n,k,求出这个程序会输出的 C[n][k]1t1051k<n105

思路

打表找规律可以发现当 k=0k=n 时输出为 1,否则为 2k。建出错误的杨辉三角,第 0 列必定为 1,第 1 列应该为第 0 列相邻两数相加,为 2;第 2 列应该为第 1 列相邻两数相加,为 2×2;第 3 列应该为第 2 列相邻两数相加,为 23。然后正确性此时已经很显然了。但是还有特殊情况,就是 k=n 的,判掉就好了。快速幂直接做。

代码

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define inf 0x3f3f3f3f
int a,b[100001],c[100001];
const int mod=1e9+7;
long long jc[100001],ny[100001];
il long long qpow(long long x,long long y)
{
register long long rn=1;
while(y)
{
if(y&1)
{
rn=(rn*x)%mod;
}
x=(x*x)%mod;
y>>=1;
}
return rn;
}
il long long Cc(int x,int y)
{
if(!y||x==y)
{
return 1;
}
else
{
return qpow(2,y);
}
}
int main()
{
scanf("%d",&a);
for(ri i=1;i<=a;i++)
{
scanf("%d",&b[i]);
}
for(ri i=1;i<=a;i++)
{
scanf("%d",&c[i]);
}
for(ri i=1;i<=a;i++)
{
printf("%lld\n",Cc(b[i],c[i]));
}
return 0;
}

C. QED's Favorite Permutation

题目内容

给定一个 n 的排列 p 和一个长为 n 的字符序列 s。如果 si=L,那么我们可以在任意时刻交换 pi1pi;如果 si=R,那么我们可以在任意时刻交换 pi+1pi。保证 s 只包含以上两种字符。现在对序列 s 进行 q 次修改,每次给定一个位置 x,如果 sx=L,将其变为 R;否则将其变为 L。询问每次修改后是否可以通过若干次操作使排列 p 单调递增。

思路

排列变有序,那么每个数该去的位置是好找的。如果所有该往左走的数都满足条件,那么整个序列也就可以变为有序的了。于是首先维护需要在哪些位置向左走,假设 i 需要向左移动到 j,那么从 j+1i 的所有点都需要可以向左。由于我们从前到后遍历排列 p,所以我们每个需要向左的区间的右端点是单调的,直接 vector 维护;然后对每个需要向右的位置赋值为 1,再把每个字符贡献的位置 +1,能达到目标就是序列里没有负数。可以线段树做,也可以直接在序列上操作。前者复杂度 O(nlogn),后者复杂度 O(n)。下面只给出 O(n) 解法代码。

代码

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define inf 0x3f3f3f3f
int a,b,c,d[200002],u,can[200002],num;
vector<pair<int,int>>vec;
char e[200002];
int main()
{
scanf("%d",&a);
while(a--)
{
scanf("%d%d",&b,&c);
vec.clear();
for(ri i=1;i<=b;i++)
{
scanf("%d",&d[i]);
if(d[i]<i)
{
while(!vec.empty())
{
if(vec.back().first>=d[i]+1)
{
vec.pop_back();
}
else
{
break;
}
}
if(!vec.empty()&&vec.back().second>=d[i]+1)
{
vec.back().second=i;
}
else
{
vec.push_back({d[i]+1,i});
}
}
}
scanf("%s",e+1);
memset(can,0,sizeof(can));
num=0;
for(register auto i:vec)
{
for(ri j=i.first;j<=i.second;j++)
{
can[j]=-1;
num++;
}
}
for(ri i=1;i<=b;i++)
{
if(e[i]=='L')
{
if(can[i]<0)
{
num--;
}
can[i]++;
}
else
{
if(can[i+1]<0)
{
num--;
}
can[i+1]++;
}
}
while(c--)
{
scanf("%d",&u);
if(e[u]=='L')
{
e[u]='R';
can[u]--;
if(can[u]<0)
{
num++;
}
if(can[u+1]<0)
{
num--;
}
can[u+1]++;
}
else
{
e[u]='L';
can[u+1]--;
if(can[u+1]<0)
{
num++;
}
if(can[u]<0)
{
num--;
}
can[u]++;
}
if(!num)
{
puts("YES");
}
else
{
puts("NO");
}
}
}
return 0;
}

D. Card Game

题目内容

一种双人卡牌游戏中有 n×m 张卡牌,每张卡牌 i 由两个数描述:花色 xi 和 等级 yi。花色编号从 1n,等级编号从 1m 。规定卡牌 i 能击败卡牌 j 当且仅当满足下面两个条件之一:

  • xi=1,xj1

  • xi=xj,yi>yj

游戏开始时将牌均分。规定 1 号玩家能够获胜,当且仅当存在一种将两人获得的牌一一对应的方案,使得 1 号玩家的每张牌都可以击败对应的 2 号玩家的牌。给定 n,m,求有多少种方案使得 1 号玩家获胜。答案对 998244353 取模。保证 m 是偶数。

思路

首先忽略掉花色 1 产生的影响,只考虑在同一个花色内的合法方案。这个直接 dp 即可,设 fi,j 表示考虑了前 i 个数有 j 个数还没有匹配(在 2 手上)的方案数,转移考虑该位被 1 还是 2 拿走,也就是 fi,j=fi1,j1+fi1,j+1。容易证明 j0 的方案一定没有任何作用。然后加上 1 的影响,再来个 dp。由于除了 1 花色之外别的花色是相互独立的,找找规律发现别的花色从 1 这里“搬救兵”,每次一定搬 2 个。这时,我们前一个 dp 的状态设计就巧妙地和这里的解法结合了起来:设 gi,j 表示考虑了前 i 个花色(不包含 1),有 j 对数等待匹配的方案数,转移枚举当前花色贡献的等待匹配的对数。最后求答案把每个对数对应的方案数乘上对应的 1 花色方案即可。这里可以再求一遍,但是有一个好用的结论:有 i 对数等待匹配的方案树和还可以再匹配 i 对数的方案数相同,证明考虑把玩家 1 的选牌集合和玩家 2 的选牌集合互换。然后直接求 i=0m2fm,i×gn1,i。无需任何优化,复杂度 O(n3)

代码

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define inf 0x3f3f3f3f
int a,b;
const int mod=998244353;
long long dp[505][505],ans,jc[1001],ny[1001],num[505],gt[505][505];
il long long qpow(long long x,long long y)
{
register long long rn=1;
while(y)
{
if(y&1)
{
rn=(rn*x)%mod;
}
x=(x*x)%mod;
y>>=1;
}
return rn;
}
il long long C(int x,int y)
{
return (((jc[x]*ny[x-y])%mod)*ny[y])%mod;
}
int main()
{
scanf("%d%d",&a,&b);
jc[0]=ny[0]=1;
for(ri i=1;i<=a+b;i++)
{
jc[i]=(jc[i-1]*i)%mod;
ny[i]=qpow(jc[i],mod-2);
}
dp[0][0]=1;
for(ri i=1;i<=b;i++)
{
for(ri j=0;j<=i;j++)
{
if(j)
{
dp[i][j]=(dp[i][j]+dp[i-1][j-1])%mod;
}
if(j<i)
{
dp[i][j]=(dp[i][j]+dp[i-1][j+1])%mod;
}
}
}
if(a==1)
{
printf("%lld\n",dp[b][0]);
return 0;
}
for(ri i=0;i<=(b>>1);i++)
{
gt[1][i]=dp[b][i<<1];
}
for(ri i=2;i<a;i++)
{
for(ri j=0;j<=(b>>1);j++)
{
for(ri k=0;k<=j;k++)
{
gt[i][j]+=(gt[i-1][k]*dp[b][(j-k)<<1])%mod;
gt[i][j]%=mod;
}
}
}
for(ri i=0;i<=(b>>1);i++)
{
ans+=(dp[b][i<<1]*gt[a-1][i])%mod;
ans%=mod;
}
printf("%lld",ans);
return 0;
}

E. Long Way to be Non-decreasing

题目内容

给你一个长为 n 的数列 a 和一个长为 m 的数列 b,保证 1ai,bim。定义一次操作如下:

  • 选定一个下标集合 S

  • 对于 iS,aibai

输出使 a 单调不降的最小操作次数,无解输出 1

思路

首先注意到由于集合任选,所以答案是具有显然的单调性的,于是二分答案。如果暴力 O(n2) check 可以喜提 16pts 高分。暴力的思路大概就是对于当前的 mid 贪心找能到达的、不小于上一个数的2最小值,找不到就返回 false。b 序列内元素相互映射,如果连边,应该是恰好 m 条边,于是想到基环树。事实上连出来的是一个内向基环树森林。然后 check 中的可达转化为基环树上距离 mid。问题来到如何处理基环树上距离。分情况讨论:

  • 不在一个连通块里,必不可达。

  • 在同一棵树上,终点不在环上。以环为根,但是二者不在同一颗子树内,或者在同一颗子树内但是不是祖先关系,则必不可达;否则就是二者的深度差。

  • 终点在环上。距离分为从子树走到环上的距离和环上距离。前一个和前面的处理方法相同,后一个可以给环钦定一个起点,然后处理出每个点离起点的距离。

但是到了这一步好像 check 还是 O(n2) 的。但是注意到我们在 check 里构造的那个序列里的元素一定单调不降,于是枚举是从前一个元素开始枚举即可,均摊就是 O(n) 的了。总复杂度 O(Tnlogn)T 是多测数据组数。

代码

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define inf 0x3f3f3f3f
int a,b,c,d[1000001],e[1000001],f[1000001],g,cnt,dfs[1000001],low[1000001],len[1000001],dep[1000001],in[1000001],out[1000001],sum[100001],be[1000001],hn[1000001],ua;
stack<int>use;
vector<int>vec[1000001];
bool col[1000001],ck[1000001];
struct BCJ
{
int fa[1000001],siz[1000001];
il void build(int x)
{
for(ri i=1;i<=x;i++)
{
fa[i]=i;
siz[i]=1;
}
}
il int find(int x)
{
if(fa[x]!=x)
{
fa[x]=find(fa[x]);
}
return fa[x];
}
il bool merge(int x,int y)
{
x=find(x),y=find(y);
if(x==y)
{
return false;
}
if(siz[x]<siz[y])
{
swap(x,y);
}
fa[y]=x;
siz[x]+=siz[y];
return true;
}
}tree;
struct node
{
int h,to;
}pic[1000001];
il void ndin(int x,int y)
{
g++;
pic[g].to=y;
pic[g].h=f[x];
f[x]=g;
}
void tarjan(int x)
{
cnt++;
dfs[x]=low[x]=cnt;
ck[x]=true;
use.push(x);
for(ri i=f[x];i;i=pic[i].h)
{
ri y=pic[i].to;
if(y==x)
{
ua++;
hn[x]=ua;
col[x]=true;
use.pop();
sum[ua]++;
ck[x]=false;
return;
}
if(!dfs[y])
{
dep[y]=dep[x]+1;
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(ck[x])
{
low[x]=min(low[x],dfs[y]);
}
}
if(dfs[x]==low[x])
{
if(use.top()!=x)
{
ua++;
while(use.top()!=x)
{
ri i=use.top();
hn[i]=ua;
col[i]=true;
len[i]=dep[i]-dep[x];
ck[i]=false;
use.pop();
sum[ua]++;
}
ck[x]=false;
col[x]=true;
hn[x]=ua;
len[x]=0;
use.pop();
sum[ua]++;
}
else
{
ck[x]=false;
use.pop();
}
}
}
void dfs0(int x,int y)
{
be[x]=y;
cnt++;
in[x]=cnt;
for(ri i:vec[x])
{
if(!col[i])
{
dep[i]=dep[x]+1;
dfs0(i,y);
}
}
out[x]=cnt;
}
il int dist(int x,int y)
{
if(x==y)
{
return 0;
}
if(tree.find(x)!=tree.find(y))
{
return inf;
}
if(col[y])
{
if(len[y]>len[be[x]])
{
return dep[x]+len[y]-len[be[x]];
}
else
{
return dep[x]+(sum[hn[y]]-(len[be[x]]-len[y]))%sum[hn[y]];
}
}
if(be[x]!=be[y]||in[y]>=in[x]||out[y]<in[x])
{
return inf;
}
return dep[x]-dep[y];
}
il bool check(int x)
{
ri re=1;
for(ri i=1;i<=b;i++)
{
while(re<=c&&dist(d[i],re)>x)
{
re++;
}
if(re>c)
{
return false;
}
}
return true;
}
int main()
{
scanf("%d",&a);
while(a--)
{
scanf("%d%d",&b,&c);
register bool te=true;
for(ri i=1;i<=b;i++)
{
scanf("%d",&d[i]);
if(d[i]<d[i-1])
{
te=false;
}
}
fill(f+1,f+1+c,0);
g=0;
tree.build(c);
for(ri i=1;i<=c;i++)
{
vec[i].clear();
}
for(ri i=1;i<=c;i++)
{
scanf("%d",&e[i]);
ndin(i,e[i]);
vec[e[i]].push_back(i);
tree.merge(i,e[i]);
}
if(te)
{
puts("0");
continue;
}
cnt=0;
fill(dfs+1,dfs+1+c,0);
fill(sum+1,sum+1+c,0);
fill(col+1,col+1+c,false);
ua=0;
for(ri i=1;i<=b;i++)
{
if(!dfs[i])
{
dep[i]=0;
tarjan(i);
}
}
for(ri i=1;i<=c;i++)
{
if(col[i])
{
dep[i]=0;
cnt=0;
dfs0(i,i);
}
}
ri m=1,n=c;
while(m!=n)
{
ri l=(m+n)>>1;
if(check(l))
{
n=l;
}
else
{
m=l+1;
}
}
if(!check(m))
{
puts("-1");
}
else
{
printf("%d\n",m);
}
}
return 0;
}

F. Many Games

题目内容

给你 n 个物品,每个物品有一个概率 pi% 和权值 wi,设 S 表示物品集合,定义如下函数:

f(s)=iS,(ipi100)×(iwi)

f(S) 的最大值。

思路

以下推式子部分默认 p 表示概率,也就是乘过 1100

这个东西不好直接 dp 答案,所以把函数中的一个东西放到 dp 状态里。显然你很难把概率放到状态上,所以设 dpi,j 表示前 i 个物品总和为 j 的最大概率,直接跑背包。但是这样还仅仅是暴力。于是剪枝。首先概率为 100 的一定选,放到最后统一算贡献;首先概率为 0 的一定不选,直接忽略;但是这样还不够优秀。然后充分发扬人类智慧找找性质,或者依托强大的观察能力打表找规律,发现选中的东西不会很多。具体有多少,需要推式子。首先在最优状态下一定不会再多选或少选一个东西(废话)。然后对于概率相同的,一定选权值更大的更优。假设在当前概率 p 已经选了 i 个数,下一个选了会更优,也就是:

pij=1iwj<pi+1(j=1iwj+wj+1)j=1iwj<p(j=1iwj+wj+1)

由于我们要贪心选大的,所以 wj+1 一定不比之前任何一个数大,于是得到 j×wj+1j=1iwj,即 wj+1j=1iwjj。带入原式,得:

j=1iwj<p×(j+1)×j=1iwjj1<p×j+1jjj+1<pj<p×j+p(1p)×j<pj<p1p

于是就可以在这一步进行剪枝。只有满足上面条件的物品才可能被选中。拿个程序跑一下会发现这东西不大,378。但是我们 w 的值域依然很大。再次充分发扬人类智慧找找性质,或者依托强大的观察能力打表找规律,发现值域不会很大。设最优答案的权值和为 W,概率为 P,由于拿走一个答案一定会变得不优,继续根据答案单调性推式子:

W×P>(Wwi)×PpiW>(Wwi)piW×pi>WwiW×(pi1)>wiW×(1pi)<wiW<wi(1pi)

1pi 的最小值为 0.01wi<2×105,推出来的这个结果貌似最大是 2×107 的。但是由于 pi×wi2×105 的(这个是输入的 p,而分子取最大值时,pi 取最小值,分母取到最大值;同理,分母取到最小值时分子取到最小值。不是很好分析,于是写个程序跑一下,发现最大值是 202020,这样值域就是 2e5 量级的了。总复杂度为 378×202020=76763560,到了能跑的水平。

代码

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define inf 0x3f3f3f3f
int a,u,v,b;
vector<int>vec[101];
long double dp[220022],ans;
int main()
{
scanf("%d",&a);
for(ri i=1;i<=a;i++)
{
scanf("%d%d",&u,&v);
vec[u].push_back(v);
}
vec[0].clear();
for(ri i=1;i<=99;i++)
{
sort(vec[i].begin(),vec[i].end());
reverse(vec[i].begin(),vec[i].end());
while(vec[i].size()>100/(100-i))
{
vec[i].pop_back();
}
}
vec[100].push_back(0);
while(vec[100].size()>1)
{
vec[100][0]+=vec[100].back();
vec[100].pop_back();
}
dp[0]=1;
for(ri i=1;i<=99;i++)
{
for(ri j:vec[i])
{
for(ri k=200000;k>=j;k--)
{
dp[k]=max(dp[k],dp[k-j]*i/100);
}
}
}
for(ri i=0;i<=200000;i++)
{
ans=max(ans,dp[i]*(i+vec[100][0]));
}
printf("%.8Lf",ans);
return 0;
}
posted @   一位很会的教授er~  阅读(50)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示