NOIP2021提高组题目解析
我真的是太菜了,....,随便点开个NOIP提高组的题目就不会做,真的是怀疑当初自己是怎么拿到省一的.....
P7961 [NOIP2021] 数列
先来看一道有意思的计数题,(计数题真的是我的弱项...)
题意大概:给定数列\(v_0,v_1,...,v_m\),从\(0,1,...,m\)中选出n个组成序列\(a_i\)(可以重复选)。要求\(2^{a_0}+2^{a_1}+...+2^{a_m}\)的二进制下1的个数不大于k,问所有\(v_{a_i}\)的乘积和。
首先我们考虑选择的序列a,因为要考虑到进位的问题,所以我们不妨规定a是不减的。接下来考虑怎么处理这个二进制下1的个数的问题。一个比较好的想法就是将当前的进位我们都停留在当前的i。就是我们把他的进位都转化成我们当前正在处理的数上,这样方便我们量化和处理。根据这些,我们可以大致的设一个状态\(f[i][j][k][num]\)表示当前选到了第i个数,数字用到了j,且之前所有小于j的进位相当于k,当前二进制和下的1的个数为num.关于状态转移,我们可以考虑数字j用了多少个,于是就有
\(f[i][j][k][num]=\sum(C_i^x*f[i-x][j-1][k-pocent(num)+pocnet(num-x)][temp]*v[j]^x)\),其中pocent(x)表示x的二进制下1的个数,temp/2+x=num.这样就完美的解决了问题。考虑时间复杂度,\(O(n^4m)\)大概可以的。
注意初始化,和很多的边界问题(毕竟我也卡了很久...,可能是因为我太菜了...呜呜呜!)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=35,M=105,P=998244353;
int n,m,K,v[M],po[N];
ll f[N][M][N][N],jc[N],jc_inv[N];
//f[i][j][k][num]表示前i个,选到了数字j,
//当前累计到数字j的进位为k,当前的二进制数中1的个数为num的乘积和。
inline ll power(ll x,ll y)
{
ll ans=1;
while(y)
{
if(y&1) ans=ans*x%P;
y>>=1;
x=x*x%P;
}
return ans%P;
}
inline void prework()
{
int n=34;
jc[0]=1;jc_inv[0]=1;
for(int i=1;i<=n;++i) jc[i]=jc[i-1]*i%P;
jc_inv[n]=power(jc[n],P-2);
for(int i=n-1;i>=1;--i) jc_inv[i]=jc_inv[i+1]*(i+1)%P;
for(int i=0;i<=n;++i)
{
int ct=0,x=i;
while(x)
{
if(x&1) ct++;
x>>=1;
}
po[i]=ct;
}
}
inline ll C(int n,int m)
{
return jc[n]*jc_inv[n-m]%P*jc_inv[m]%P;
}
int main()
{
// freopen("1.in","r",stdin);
scanf("%d%d%d",&n,&m,&K);
for(int i=0;i<=m;++i) scanf("%d",&v[i]);
prework();
// for(int i=1;i<=n;++i)
// f[i][0][i][po[i]]=power(v[0],i);
for(int i=1;i<=n;++i)//前i个数
for(int j=0;j<=m;++j)//用到了数字j
{
for(int k=0;k<=i;++k)//累计到当前数字j的进位
for(int l=po[k];l<=n;++l)//二进制下1的个数.
{
for(int x=0;x<=k;++x)//枚举当前数字j的个数。
{
if(i==x&&k==i&&l==po[i]) f[i][j][k][l]=power(v[j],x);
else
{
if(l-po[k]+po[k-x]<=0) continue;
int t1=(k-x)*2;
if(t1<=i-x&&j>=1) f[i][j][k][l]=(f[i][j][k][l]+C(i,x)*f[i-x][j-1][t1][l-po[k]+po[k-x]]%P*power(v[j],x)%P)%P;
if(t1+1<=i-x&&j>=1) f[i][j][k][l]=(f[i][j][k][l]+C(i,x)*f[i-x][j-1][t1+1][l-po[k]+po[k-x]]%P*power(v[j],x)%P)%P;
}
}
// printf("%d %d %d %d %lld\n",i,j,k,l,f[i][j][k][l]);
}
}
ll ans=0;
for(int i=0;i<=n;++i)
for(int j=0;j<=K;++j)
ans=(ans+f[n][m][i][j])%P;
printf("%lld",ans);
return 0;
}
P7962 [NOIP2021] 方差
接下来看这个题,也是很有趣的一道题。
简化题意:给定数列\(a_i\),你可以进行若干次操作,每次操作都选定一个\(i,1<i<n\),然后将\(a_{i+1}+a_{i-1}-a_i\)替换掉\(a_i\)。最后要求整个数列的方差最小,输出这个最小的方差乘\(n^2\)的结果。
首先需要将答案变一下形式,通过简单的变换可以得知最后要求的答案为\(ans=n\times\sum_{i=1}^n a_i^2-sum^2\),简单的观察发现没什么性质,就先放到这。
之后观察这个替换到底有什么性质,\(a_i\)替换为\(a_{i+1}+a_{i-1}-a_i\),其实和容易发现是\(a_i\)两边的数与\(a_i\)的关系,考虑我们学过什么也是这个样子的?差分嘛!我们观察他们的差分有什么变化,\(a_{i-1},a_{i},a_{i+1}\)的差分为\(a_{i-1},a_i-a_{i-1},a_{i+1}-a_i\);\(a_{i-1},a_{i+1}+a_{i-1}-a_i,a_{i+1}\)的差分为\(a_{i-1},a_{i+1}-a_i,a_i-a_{i-1}\),惊奇的发现数值竟然没有发生变化,只是位置发生了变化。说明这些操作是能将差分序列就行位置上的重排。那接下来考虑差分上怎样的位置会使得上面式子的答案最小。
这个时候就有一些考试技巧在里面了,如果实在考场上的话,完全可以观察一下样例的性质,还可以打表,多找一下规律,这样的话,可以节省思维难度。
接下来继续推上面的式子,由于是求方差,所以我们可以将整个数列中的每一个数都减去某个数,最后的结果是不变的。那我们就将上面的式子继续修改,尽可能的想差分靠近。我们先假设\(d_i=a_{i+1}-a_i\)
\(ans=n\times\sum_{i=1}^n a_i^2-sum^2\)
\(=n\times\sum_{i=1}^n (a_i-a_1)^2-(\sum_{i=1}^n a_i-a_1)^2\)
\(=n\times\sum_{i=1}^{n-1}(\sum_{j=1}^{i}d_j)^2-(\sum_{i=1}^{n-1}\sum_{j=1}^{i}d_j)^2\)
\(=n\times\sum_{i=1}^{n-1}\sum_{j=1}^{i}\sum_{k=1}^{i}d_j*d_k-(\sum_{i=1}^{n-1}(n-i)*d_{i})^2\)
\(=n\times\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*(n-max(j,x))-\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*(n-j)*(n-k)\)
\(=\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*((j+k-max[j,k])*n-j*k)\)
\(=\sum_{j=1}^{n-1}\sum_{k=1}^{n-1}d_j*d_k*(min[j,k]*n-j*k)\)
\(=\sum_{i=1}^{n-1}i*d_i^2*(n-i)+2*\sum_{i=1}^{n-1}\sum_{j=i+1}^{n-1}d_i*d_j*i*(n-i)\)
观察这个式子,发现前一半的式子值是固定的,我们想要后一半的式子尽可能的小,发现这个式子中出现次数最多的乘积是中间项。所以答案最小的话,差分序列应该是呈现一个先减后增的形式。考虑先将所有的差分先排序,之后依次考虑它查到当前序列的前段还是后端,采用dp的形式,推导后的式子太麻烦,还是回到最初的式子,\(ans=n\times\sum_{i=1}^n a_i^2-sum^2\)。发现这种形式可以用dp很方便的处理,看看我们需要什么量,发现还需要保存当前所有\(a_i\)的和,于是我们设f[i][x]表示放到了第i个差分,当前所有ai的和是x的所有ai的平方的和的最大值。至于转移就很正常了。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e4+10,M=6e6+10;
const ll qwq=0x3f3f3f3f3f3f3f3f;
int n,a[N],d[N],s[N],mx;
ll f[2][M];
int main()
{
// freopen("1.in","r",stdin);
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
for(int i=2;i<=n;++i) a[i]-=a[1];
a[1]=0;mx=a[n];
for(int i=2;i<=n;++i) d[i-1]=a[i]-a[i-1];
sort(d+1,d+n);
for(int i=1;i<n;++i) s[i]=s[i-1]+d[i];
memset(f,0x3f,sizeof(f));
int u=0;
f[u][0]=0;
for(int i=1;i<n;++i)
{
if(d[i]==0) continue;
u=u^1;
for(int j=0;j<=mx*i;++j) f[u][j]=qwq;
for(int j=0;j<=mx*(i-1);++j)
{
if(f[u^1][j]==qwq) continue;
f[u][d[i]*i+j]=min(f[u][d[i]*i+j],f[u^1][j]+(ll)d[i]*d[i]*i+(ll)2*j*d[i]);
f[u][j+s[i]]=min(f[u][j+s[i]],f[u^1][j]+(ll)s[i]*s[i]);
}
}
ll ans=1e18;
for(int i=0;i<=mx*(n-1);++i)
if(f[u][i]!=qwq) ans=min(ans,f[u][i]*n-(ll)i*i);
printf("%lld\n",ans);
return 0;
}