[18.8.21]校内NOIP模拟赛
T1 Victory
题意
记得是16年百度之星初赛原题。
问有多少$n$以内的正整数$x$,满足没有峰。
峰的定义是,存在一个$x$,使能找到一对$i,j(i<x<j)$,满足$s_i<s_x,s_j<s_x$。
$1\leq n \leq 10^{100}$
题解
显然是数位dp。
$dp_{i,j,st1,st2}$表示当前到了第$i$位,上一位是$j$,前缀与原数前缀是否相等,当前是处于上升还是下降。
对于初值:由于不考虑前导零,所以可以枚举位数和最高位的数字。由于处于下降状态可以变为上升,而上升无法变为下降,所以可以认为初始情况处于下降状态。
转移显然。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
template<typename __T>
inline void read(__T &x)
{
x=0;
int f=1;char c=getchar();
while(!isdigit(c)){if(c=='-') f=-1;c=getchar();}
while(isdigit(c)) {x=x*10+c-'0';c=getchar();}
x*=f;
}
const int mod=1000000007;
char str[105];
int s[105];
int n;
int dp[105][2][2][10];
int main()
{
scanf("%s",str);
n=strlen(str);
for(int i=0;i<n;i++)
s[i]=str[n-i-1]-'0';
for(int i=1;i<s[n-1];i++) dp[n-1][0][0][i]=1;
dp[n-1][1][0][s[n-1]]=1;
for(int i=0;i<n-1;i++)
for(int j=1;j<=9;j++)
dp[i][0][0][j]=1;
for(int i=n-1;i>0;i--)
{
for(int j=0;j<=9;j++)
{
if(dp[i][0][0][j])
{
for(int k=0;k<=j;k++)
(dp[i-1][0][0][k]+=dp[i][0][0][j])%=mod;
for(int k=j+1;k<=9;k++)
(dp[i-1][0][1][k]+=dp[i][0][0][j])%=mod;
}
if(dp[i][0][1][j])
for(int k=j;k<=9;k++)
(dp[i-1][0][1][k]+=dp[i][0][1][j])%=mod;
}
int j=s[i];
int ne=s[i-1];
if(dp[i][1][0][j])
{
if(ne<=j)
{
for(int k=0;k<=(ne-1);k++)
(dp[i-1][0][0][k]+=dp[i][1][0][j])%=mod;
(dp[i-1][1][0][ne]+=dp[i][1][0][j])%=mod;
}
else
{
for(int k=0;k<=j;k++)
(dp[i-1][0][0][k]+=dp[i][1][0][j])%=mod;
for(int k=j+1;k<ne;k++)
(dp[i-1][0][1][k]+=dp[i][1][0][j])%=mod;
(dp[i-1][1][1][ne]+=dp[i][1][0][j])%=mod;
}
}
if(dp[i][1][1][j])
{
for(int k=j;k<=min(9,ne-1);k++)
(dp[i-1][0][1][k]+=dp[i][1][1][j])%=mod;
if(ne>=j)
(dp[i-1][1][1][ne]+=dp[i][1][1][j])%=mod;
}
}
long long ans=0;
for(int i=0;i<=9;i++)
ans=(ans+dp[0][0][0][i]+dp[0][0][1][i]+dp[0][1][0][i]+dp[0][1][1][i])%mod;
printf("%lld\n",ans);
return 0;
}
T2 技能大赛
题意
给你一张有$n$个点,$m$条边的图,求所有满足条件的点集的价值之和。
满足条件是指,每条边的至少一个顶点被选中。
点集的价值是所有点的乘积。
$1\leq n \leq 36$。
题解
显然,这题可以用折半搜索。
具体来讲,先将所有点分成左右两类,对于左边的一类,枚举选择的方案,之后计算两个顶点均在左侧的边是否满足条件。如果满足条件,再求出右侧至少要有哪些点需要被选中。显然,这个右侧最小集合的所有超集都是满足条件的。
因此可以先求出右侧点中,所有内部边都被满足的选择方案,之后做一个类似子集和变换(高维前缀和?)的操作。然后对左侧点进行枚举,算出对右侧的依赖,之后直接累加答案(答案变化量为左侧的价值乘右侧所有方案的价值和,结论显然)。
一个结论是,合法的选择方案,一定满足:对于所有未被选中的点,其直接连点均被选中。
因此我们可以处理一个掩码,表示哪些点与这些点链接,在判断的时候就可以做到$O(1)$判断了。
所以总时间复杂度是$O(m+2^nn)$。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
template<typename __T>
inline void read(__T &x)
{
x=0;
int f=1;char c=getchar();
while(!isdigit(c)){if(c=='-') f=-1;c=getchar();}
while(isdigit(c)) {x=x*10+c-'0';c=getchar();}
x*=f;
}
const int magi=18;
int mod;
int n,m;
bool mp[50][50];
long long mask[50];
long long ans=0;
long long cnt[1<<18];
int aa[50];
int main()
{
read(n);
read(m);
read(mod);
int a,b;
for(int i=0;i<n;i++)
read(aa[i]);
for(int i=0;i<m;i++)
{
read(a);
read(b);
a--;
b--;
mp[a][b]=mp[b][a]=1;
mask[a]|=(1ll<<b);
mask[b]|=(1ll<<a);
}
if(n<=20)
{
for(int st=0;st<(1<<n);st++)
{
bool fg=0;
long long np=1;
for(int i=0;i<n;i++)
{
if((st>>i)&1)
np=np*aa[i]%mod;
if(((st>>i)&1)==0 && (st|mask[i])!=st)
{
fg=1;
break;
}
}
if(!fg) (ans+=np)%=mod;
}
ans%=mod;
printf("%lld\n",ans);
return 0;
}
int mmm=(1<<magi)-1;
for(int st=0;st<(1<<magi);st++)
{
bool fg=0;
long long ans=1;
for(int i=0;i<magi;i++)
{
if((st>>i)&1)
ans=ans*aa[i]%mod;
if(((st>>i)&1)==0 && (st|(mask[i]&mmm))!=st)
{
fg=1;
break;
}
}
if(!fg)
(cnt[st]+=ans)%=mod;
}
for(int i=0;i<magi;i++)
for(int st=0;st<(1<<magi);st++)
if(((st>>i)&1)==0) (cnt[st]+=cnt[st|(1<<i)])%=mod;
int mxx=n-magi;
for(int st=0;st<(1<<mxx);st++)
{
bool fg=0;
long long np=1;
for(int i=0;i<mxx;i++)
{
if((st>>i)&1)
np=np*aa[i+magi]%mod;
if(((st>>i)&1)==0 && (st|(mask[i+magi]>>magi))!=st)
{
fg=1;
break;
}
}
if(fg) continue;
int msk=0;
for(int i=0;i<magi;i++)
if((st|(mask[i]>>magi))!=st) msk|=(1<<i);
(ans+=cnt[msk]*np)%=mod;
}
printf("%lld\n",ans);
return 0;
}
T3 冒泡排序2
题意
给定$n,k$,求有多少种排列满足,进行$k$ 轮冒泡排序外层循环后,数列是“几乎正确”的。
几乎正确的定义是:最长上升子序列至少为$n-1$。
$1\leq n,k \leq 50$。
题解
这题由于最开始特判写假了所以多了些没有用的代码。。
首先考虑,几乎正确的序列(已经排序好的序列),一定可以由如下操作得到:
从一个已经排好序的序列内随意抽出一个数放到其它位置。
在之前已经得到一条结论,进行$k$次操作后的序列,一定是之前剩下的和后面$k$个数的最小值。
显然对于一段已经排好序的序列来讲,每一个数都有$k+1$种位置可以放(除了末尾位置不够的情况)。
考虑如下两种移动数的情况:
如果是将数字$x$向前移,那么被移动的这个数字也是自由的(有$k+1$种位置可以放),但是后面的数字只有$1$种位置可以放置(因为后面的数字比它小,所以只能放到它影响不到的地方,显然只有一个)。同理,后面第二个数字也只有$1$种情况(因为原数字影响不到的地方有两个,但是之前已经占了一个,所以只剩一个)。这种情况直到遇到一个比原数字$x$大的数字。之前这段相当于整体往右侧平移了$k$格,因此必须要满足,大于$x$的数字有至少$k$个。
那么对于所有比$x$大的数字,放置方案是自由的,有$k+1$种(包含了之前空出的一段以及后面新增的部分,末尾可能会不够,此时用阶乘计算,下面同理)。
如果是数字$x$向后移,显然对于所有位置在数字$x$之前的数字,均是自由的。
对于数字$x$来讲,由于$x$前面的数字比它大,所以$x$的原位置必须在上一个数字影响不到的地方(所以当$x$后面的数字不足$k$个的时候不合法),显然只有一种选择。
对于在数字$x$之后的数字,由于均比$x$大,所以也是自由的(排除末尾位置不够)。
注意去重。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
template<typename __T>
inline void read(__T &x)
{
x=0;
int f=1;char c=getchar();
while(!isdigit(c)){if(c=='-') f=-1;c=getchar();}
while(isdigit(c)) {x=x*10+c-'0';c=getchar();}
x*=f;
}
int n,k,mod;
int main()
{
read(n);
read(k);
read(mod);
k=min(n-1,k);
long long aans=0;
for(int l=1;l<=n;l++)
for(int r=l+1;r<=n;r++)
{
long long ans=1;
if(n-r>=k)
{
int rem=n-r+l;
for(int i=k+1;i<=rem;i++)
(ans*=(k+1))%=mod;
for(int i=1;i<=k;i++)
(ans*=i)%=mod;
aans=(aans+ans)%mod;
}
if(r==l+1) continue;
if(r<=n-k)
{
ans=1;
int rem=n-1;
for(int i=k+1;i<=rem;i++)
(ans*=(k+1))%=mod;
for(int i=1;i<=k;i++)
(ans*=i)%=mod;
aans=(aans+ans)%mod;
}
}
int rem=n;
long long ans=1;
for(int i=k+1;i<=rem;i++)
(ans*=(k+1))%=mod;
for(int i=1;i<=k;i++)
(ans*=i)%=mod;
aans=(aans+ans)%mod;
printf("%lld\n",aans);
return 0;
}