我真的是太菜了,....,隨便點開個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;
}