第一题:Alice和Bob是好朋友,她们经常喜欢在一起玩石子游戏。这一次他们想出了一个新 玩法:有若干堆石子,要求每次在一堆数量不为1的石子堆中取出石子,假设这堆石子 的个数是x,那么允许取的个数为正整数d,要求d|x且d ≠ x。如果没有办法再取石子了, 需要操作的那一方就输了。 游戏初始有m堆石子,每堆石子的个数均为1到n之间的正整数。Alice和Bob都是绝 对聪明的,而Alice是先手,她想知道有多少种可能的初始状态自己一定能获胜呢?这里 我们认为石子堆是有顺序的,例如(2,3,2)和(3,2,2)被认为是两种不同的初始状态。。
这道题我凭着那一点点博弈论基础,硬是推了两个小时,但是是错的,以为我压根没考虑sg值,只是考虑了必胜态和必败态的异或。
首先一堆石子的sg值可以直接打表观察,发现是。
现在我们来证明一下sg定理。也就是说。
证明这条式子,我们只要两个东西。
首先,证明对于任意的,存在,且Y为X的后继状态。
我们设,k为M中最高的那一位,因为 A>B,所以k那一位一定是A贡献的,而恰好说明了存在的第k位为1。我们将令。显然K
其次,我们要证明对于Y为X的后继状态,。
它的一个后继状态一定,把x中的其中一位改变为了x的后继状态,而x和x的后继状态的sg值肯定不同。证毕。
那么我们很容易可以开一个数组记录一下sg等于i的x有多少个。做FWT然后倍增点乘就好了。
#include
#include
#include
#include
using namespace std;
const long long mod=998244353;
long long p[100010],op[100010];
long long block=1e8,m;
long long a[2010];
char s[10010];
int len=0,t,limit;
void prepare(){
int begin=1,ti=0;t=(len+7)/8;
while(begin<=t){
long long temp=0,nowans=0;
for(int i=begin;i<=t;i++)
temp=temp*block+a[i],a[i]=temp/2,temp%=2,(nowans=nowans*block+a[i])%=mod;
if(temp==1) nowans++;
p[ti]=nowans%mod;
while(begin<=t && a[begin]==0) begin++;
ti++;
}
limit=1;
while(limit=2;l/=2)
for(int i=0;i=1;i-=8){
tot=0;
for(int j=max(i-7,1);j<=i;j++) tot=tot*10+s[j]-'0';
a[t]=tot;t--;
}
prepare();
solve();
}
第二题:Nathan是一位远近闻名的大魔法师,今天她从魔法卷轴上看到了一种古老的奥术模 型。这种奥术模型强大无比,但施法时不够稳定。准确地说,这种模型由一个长度为n 的咒语串组成,每个咒语都是一个1到9的数字。施法时Nathan吟唱出其中一个子串,反 应时将会从这个子串中再次随机选择一个子串,并释放出等同于把子串转成十进制数字 的能量。例如咒语串为12345,Nathan吟唱出子串[2,4],则实际上产生的能量可能为2、 3、4、23、34、234这六种情况之一。 更为神奇的是,奥术模型可能产生能量跃迁。能量跃迁发生时,咒语串的某个子串 会被另一个等长的子串替换。例如咒语串为12345,子串[2,4]被子串[3,5]替换后变为 13455。 Nathan想让作为学徒的你负责这个模型的维护工作,即你需要执行Q个操作。第一 类操作是Nathan选择了一个子串[L,R]吟唱,你需要回答这次施咒期望产生的能量,输 出模998244353意义下的结果。第二类操作是模型产生了一次能量跃迁,子串[L,L+x] 被子串[R,R+x]替换。
这题打的暴力一个点都过不去。
考虑O(n)计算区间内的贡献。,这个想一想就可以知道了。
我们来化简一下公式,让它变得好维护:
。
那么我们直接用线段树维护。
就可以获得60分的好成绩。带修改需要加上一个可持久化平衡树。(菜鸡不会
第三题:对于一个正整数n,定义一个消除操作为选定[L,R,x],如果n的第L到第R位上的数 字都≥x,并且这些数都相等,那么该操作合法(从低位到高位编号从1开始),并将这些位数 上的数减x。 定义n的最小操作数为对一个数操作最少的次数使得这个数变成0的次数。如1232的最 小操作数为3,一个合法解是[2,2,1],[1,3,2],[4,4,1]。 试求[L,R]内最小操作数为k的数的个数。
时间复杂度是对的,但是不知道为什么调不出来。
实际上可以直接考虑爆搜,但是不要考虑循环,因为那样子没有什么前途,至少我们爆搜还是可以很方便的记忆化和剪枝的。
那么就要考虑如何过一遍这个数求得它的最小操作数了。
考虑单调栈,维护一个从小到大的单调栈。每加入一个数就把之前<=它的数给弹掉,并给答案++。注意考虑一下相等的情况就可以了。然后因为单调栈里面的元素不会重复,就可以把前面的状态记录下来了,当然是数位Dp那种记录。前面留下来的状态用一个2^9的数组就可以存下来了。具体看代码
#include
#include
#include
#include
using namespace std;
long long l,r;
int k;
long long f[20][512];
int a[20],b[20];
int p[20],top=0;
int coef=0;
long long get_ans(int*op,int now,bool full,int tot){
if(tot+top+nowk) return 0;
if(now==0) return 1;
if(!full && f[now][coef]) return f[now][coef];
int bound=(full?op[now]:9),v[top+1],temp=coef,t=0;
long long ans=0;
bool tf;
v[0]=top;for(int i=1;i<=v[0];i++) v[i]=p[i];
for(int i=bound;i>=0;i--){
tf=false;
while(i=1) t++,coef^=(1<<(p[top]-1)),top--;
if(i!=0 && p[top]!=i) p[++top]=i,coef^=(1<<(i-1)),tf=true;
ans+=get_ans(op,now-1,full&(i==bound),tot+t);
if(i!=0 && tf) top--,coef^=(1<<(i-1));
}
top=v[0];for(int i=1;i<=top;i++) p[i]=v[i];coef=temp;
if(!full) f[now][coef]=ans;
return ans;
}
int main(){
scanf("%lld %lld %d",&l,&r,&k);l=l-1;
while(r) a[++a[0]]=r%10,r/=10;
while(l) b[++b[0]]=l%10,l/=10;
long long ans=get_ans(a,a[0],1,0);for(int i=1;i<=19;i++) for(int j=0;j<512;j++) f[i][j]=0;
ans-=get_ans(b,b[0],1,0);
printf("%lld\n",ans);
}