20240727比赛总结
T1 随 (rand)
https://gxyzoj.com/d/hzoj/p/710
本场最难题,因为误判了难度以及自己写矩阵快速幂的实力,浪费了接近2h
10pt:因为\(1\le a_i<mod\),所以当mod=2时,a数组只有1,所以直接输出1即可
20pts:显然,当只有一个数时,直接计算即可
50pts:当\(m\le 300\)时,直接模拟每次操作,计算最终得到每个值的概率,统计即可
100pts:
在50分的基础上继续优化,主要的时间复杂度在m次操作上
可以发现,每一次转移时,每个数所能转移到的数是固定的,所以考虑矩阵快速幂
首先开个桶,记录每个数出现的次数,将m分成\(\sum 2^i\),每次枚举完\(i\)后维护\(cnt\)数组
\(cnt_i\)表示在进行k轮后为i的方案数,在转移时,\(cnt_{i\times j\% mod}=\sum cnt_i\times cnt_j\)
代码:
#include<cstdio>
#define ll long long
using namespace std;
const int p=1e9+7;
int n,m,mod,a[100005];
ll cnt[1005],f[1005],tmp[1005];
ll qpow(ll x,int y,int q)
{
ll res=1;
while(y)
{
if(y&1) res=res*x%q;
x=x*x%q;
y>>=1;
}
return res;
}
int main()
{
scanf("%d%d%d",&n,&m,&mod);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
cnt[a[i]]++;
}
if(mod==2)
{
printf("1");
return 0;
}
if(n==1)
{
printf("%d",qpow(a[1],m,mod));
return 0;
}
ll t=qpow(qpow(n,m,p),p-2,p),ans=0;
f[1]=1;
while(m)
{
if(m&1)
{
for(int i=0;i<mod;i++)
{
tmp[i]=f[i],f[i]=0;
}
for(int i=0;i<mod;i++)
{
for(int j=0;j<mod;j++)
{
f[i*j%mod]=(f[i*j%mod]+tmp[i]*cnt[j])%p;
}
}
}
for(int i=0;i<mod;i++)
{
tmp[i]=cnt[i],cnt[i]=0;
}
for(int i=0;i<mod;i++)
{
for(int j=0;j<mod;j++)
{
cnt[i*j%mod]=(cnt[i*j%mod]+tmp[i]*tmp[j])%p;
}
}
m>>=1;
}
for(int i=0;i<mod;i++)
{
ans+=i*f[i]%p;
ans%=p;
}
printf("%d",ans*t%p);
return 0;
}
T2 单(single)
https://gxyzoj.com/d/hzoj/p/711
对于所有t=0,可以up and down直接求解
设\(f_i\)表示子树内,\(g_i\)表示子树外,\(P_i\)表示i的子树内时权值之和,则:
化简得:
即:
所以当t=1时,如果知道\(P_{rt}\),就可以差分求出其他值
考虑将上述的式子加起来,得到\(\sum k_i dp_i=(n-1)P_{rt}-2\sum P_i\)
而\(b_{rt}=\sum P_i\)
所以dfs求解即可
代码:
#include<cstdio>
#define ll long long
using namespace std;
int T,n,edgenum,head[100005],a[100005],b[100005],f[100005];
int dp1[100005],dp2[100005];
struct edge{
int to,nxt;
}e[200005];
void add_edge(int u,int v)
{
e[++edgenum].nxt=head[u];
e[edgenum].to=v;
head[u]=edgenum;
}
void dfs(int u,int fa)
{
f[u]=a[u],dp1[u]=dp2[u]=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs(v,u);
f[u]+=f[v];
dp1[u]+=dp1[v];
}
dp1[u]+=f[u]-a[u];
}
void dfs1(int u,int fa)
{
if(u!=1)
{
dp2[u]+=dp2[fa]+f[1]-f[u];
dp2[u]+=dp1[fa]-dp1[u]-f[u];
}
b[u]=dp1[u]+dp2[u];
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs1(v,u);
}
}
ll sum1[100005],sum;
void dfs2(int u,int fa)
{
if(u!=1)
sum1[u]=b[u]-b[fa];
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs2(v,u);
}
}
void dfs3(int u,int fa)
{
a[u]=sum1[u];
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa) continue;
dfs3(v,u);
a[u]-=sum1[v];
}
}
int main()
{
// freopen("1.txt","r",stdin);
// freopen("2.txt","w",stdout);
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
edgenum=0;
for(int i=1;i<=n;i++)
{
head[i]=a[i]=b[i]=sum1[i]=0;
}
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_edge(u,v);
add_edge(v,u);
}
int t;
scanf("%d",&t);
if(t==0)
{
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
dfs(1,0);
dfs1(1,0);
for(int i=1;i<=n;i++) printf("%d ",b[i]);
}
else
{
for(int i=1;i<=n;i++) scanf("%d",&b[i]);
dfs2(1,0);
sum=0;
for(int i=2;i<=n;i++) sum+=sum1[i];
sum+=2ll*b[1];
sum/=(n-1);
for(int i=2;i<=n;i++)
{
sum1[i]=-(sum1[i]-sum)/2;
}
sum1[1]=sum;
dfs3(1,0);
for(int i=1;i<=n;i++) printf("%d ",a[i]);
}
printf("\n");
}
return 0;
}
T3 题(problem)
https://gxyzoj.com/d/hzoj/p/712
计数四合一
typ=0
因为要回到原点,所以相反的两个方向的步数要一致,所以可以枚举其中一个方向的总步数即可
for(int i=0;i<=n;i+=2)
{
ans+=C(n,i)*C(i,i/2)%p*C(n-i,(n-i)/2)%p;
ans%=p;
}
typ=1
因为只能在x的非负半轴上运动,所以必然在任何时刻都有向右的步数大于等于向左的步数
所以直接求卡特兰数第\(\frac{n}{2}\)项即可
printf("%d",((C(n,n/2)-C(n,n/2+1)+p)%p));
typ=2,可以使用类似背包的做法
容易想到,如果要换轴,就必须要回到原点,所以它必然是横向和纵向交替出现且每段两种方向的次数是一样的
所以,可以枚举同轴走的步数,求出方案数,然后设\(dp_i\)表示走了i步的方案数,用背包求解即可
因为开始可以走横轴也可以走纵轴,所以要乘2
代码:
for(int i=2;i<=n;i+=2)
{
f[i]=C(i,i/2);
}
dp[0]=1;
for(int i=2;i<=n;i++)
{
for(int j=2;j<=i;j+=2)
{
dp[i]+=dp[i-j]*f[j]%p;
dp[i]%=p;
}
}
printf("%d",dp[n]*2%p);
typ=3
因为x,y均大于等于0,显然卡特兰数
此时,像操作0一样枚举横向的次数即可
注意,当\(i\)和\(n-i\)中有一个是0时,直接加卡特兰数
ans=2*((C(n,n/2)-C(n,n/2+1)+p)%p)%p;
for(int i=2;i<n;i+=2)
{
int t1=(C(i,i/2)-C(i,i/2+1)+p)%p;
int t2=(C((n-i),(n-i)/2)-C((n-i),(n-i)/2+1)+p)%p;
ans+=C(n,i)*t1%p*t2%p;
ans%=p;
}
T4 DP搬运工1
https://gxyzoj.com/d/hzoj/p/2636
预设性dp
假设从小到大填数,因为新的数时当前最大的,所以当存在与他相邻的数是,直接相加即可
设\(dp_{i,j,k}\)表示目前在填i,最左和最右有数两点之间有j段空余,目前和为k
- 在两边填
- 与最左或最右相邻,此时段数不变,答案增加i
dp[i][j][k+i]=(dp[i][j][k+i]+dp[i-1][j][k]*2)%p
- 不相邻,则另起一段,答案不变
dp[i][j+1][k]=(dp[i][j+1][k]+dp[i-1][j][k]*2)%p
- 在最左和最右之间
- 放在两个数之间,且不与任何一个相邻,此时新增一段,答案不变
dp[i][j+1][k]=(dp[i][j+1][k]+dp[i-1][j][k]*j)%p
- 与一个数相邻,此时答案增加i,空余段数不变
dp[i][j][k+i]=(dp[i][j][k+i]+dp[i-1][j][k]*2*j)%p
- 将两个有数的段连接,段数-1,因为两边都有数,所以答案加2i
dp[i][j-1][k+2*i]=(dp[i][j-1][k+2*i]+dp[i-1][j][k]*j)%p
完整代码:
#include<cstdio>
#define ll long long
using namespace std;
const int p=998244353;
int n,K;
ll dp[55][55][2605];
int main()
{
scanf("%d%d",&n,&K);
dp[1][0][0]=1;
for(int i=2;i<=n;i++)
{
for(int j=0;j<=n;j++)
{
for(int k=0;k<=K;k++)
{
dp[i][j][k+i]=(dp[i][j][k+i]+dp[i-1][j][k]*2)%p;
dp[i][j+1][k]=(dp[i][j+1][k]+dp[i-1][j][k]*2)%p;
dp[i][j+1][k]=(dp[i][j+1][k]+dp[i-1][j][k]*j)%p;
dp[i][j][k+i]=(dp[i][j][k+i]+dp[i-1][j][k]*2*j)%p;
if(j) dp[i][j-1][k+2*i]=(dp[i][j-1][k+2*i]+dp[i-1][j][k]*j)%p;
}
}
}
ll ans=0;
for(int i=0;i<=K;i++) ans=(ans+dp[n][0][i])%p;
printf("%d",ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律