P2838

在正文之前,有一些小提醒:

不要习惯性的写条件语句,时刻记住只能使用瓶子。

瓶子的水量和容积不要搞混了。

制作瓶子的容积是有限的,上界为 \(1e9\)

尽量不要把写一些无意义的操作,可能会导致超过步数上限,一般正常写上限很松。

后面的测试点会经常使用之前的函数,一定要写好封装,最好把各类操作也封装起来。

我封装的操作:

int input(){cout<<"I"<<"\n";return ++cnt;}
void output(int x){cout<<"O "<<x<<"\n";}
int make(int siz){cout<<"C "<<siz<<"\n";return ++cnt;}
int copy(int s){cout<<"M "<<s<<"\n";return ++cnt;}
void pour(int x,int y){cout<<"T "<<x<<" "<<y<<"\n";}
void fill(int x){cout<<"F "<<x<<"\n";}
void empty(int x){cout<<"E "<<x<<"\n";}

\(1.a+b\)

注意到 \(a\)\(b\) 的范围很小,直接制作一个容积为上界的瓶子都倒进去即可。

int add(int a,int b){
    int ans=make(INF);
    pour(a,ans),pour(b,ans);
    return ans;
}

再次注意返回的是瓶子。

\(3.max(a,b)\)

\(2\) 个数据点需要调用第 \(3\) 个测试点。

注意到 \(T\) 操作里 \(a\) 瓶水量不够或者 \(b\) 瓶已经装满都会停下来。那么把 \(a\) 瓶里的水倒到一个和 \(b\) 瓶容积相同的水瓶 \(c\) 里,这个 \(c\) 瓶里的水就是 \(a,b\) 瓶水量的较小值。求出了最小值,把剩下的水倒在一起就是最大值。

我觉得读者应该能看懂,但是还是放个图吧,以 \(a<b\) 的情况举例。

\(c\) 瓶是复制的 \(b\) 瓶,\(d\) 瓶是容积为上界的瓶子,把 \(a\) 瓶的水倒进 \(c\) 瓶,再把 \(a,b\) 瓶里剩下的所有水倒进 \(d\) 瓶里,\(d\) 瓶里的水量就是 \(max(a,b)\)

写操作 \(3\) 的时候建议传一个 \(pair\) ,同时返回最小值和最大值,后面的测试点有用。

pii minmax(int x,int y){
    int nw=copy(y);pour(x,nw);
    return (pii){nw,add(x,y)};
}

$2.\left | a-b \right | $

发现了吗,我们直接用最大值减最小值就行了。

int del(int a,int b){
    pii p=minmax(a,b);
    int nw=copy(p.first);pour(p.second,nw);return p.second;
}

\(4.\gcd(a,b)\)

考虑正常计算最大公因数一般使用辗转相除法。但是我们发现在这题里实现带余除法是一件很困难的操作。

实际上,第 \(7\) 个数据点就是对 \(10\) 做除法,做一次除法需要约 \(1500\) 次操作,辗转相除法需要 \(\log a\) 次除法,在 \(a,b\le 1000\) 的限制下完全不如直接把一次除法拆成多次减法,实现类似的操作。

至于怎么做减法,上面就有。

int gcd(int a,int b){
    pii p=minmax(a,b);
    a=p.first,b=p.second;
    for(int i=1;i<=1000;i++){
        a=copy(a),pour(b,a);
        pii p=minmax(a,b);
        a=p.first,b=p.second;
    }
    return b;
}

\(5.\) 二进制表示

到了这个点本题才开始上难度,这同样也是后半部分的重要操作。不难想到肯定是要用倍增,但是关键在于我们现在甚至无法完成这样一个操作:

if(a>=b) a-=x;

倍增时我们确定一个位置能否填 \(1\) 后是没办法更改剩余水量的。

\(2\) 数据点只能返回绝对值,是无法完成这个操作的。

观察一下现在能做什么,倍增过程中枚举到第 \(i\) 位(从低到高数第 \(i\) 位)时,可以创建两个瓶子,一个容积是 \(2^i-1\),一个容积是 \(1\),先倒到前一个瓶子里,再把剩下的水倒到后一个瓶子里,后一个瓶子里的水量就是这个二进制位的值。

还是画个图,把现在的水量记为 \(x\),如果 \(x<2^i\),就倒不到后一个瓶子:

而如果 \(x\ge 2^i\) 就可以:

尝试实现如果后一个瓶子中有水,把 \(x\) 的水量减去 \(2^i\) 的操作。

不难发现可以新开一个和后一个瓶子一样的瓶子 \(a\),把 \(a\) 的水量左移 \(i\) 位,把 \(a\) 倒空,再用 \(x\) 填满 \(a\) 即可,因为如果后一个瓶子里没有水,\(0\) 怎么左移都还是 \(0\)

这个操作的实现是简单的,每左移一位创建一个容积无限的瓶子,把 \(a\) 倒进去两次,把这个瓶子里的水用 \(M\) 操作复制出来即可。

int lft(int a,int x){
    int nw=make(INF),aa=copy(a);
    fill(aa);pour(aa,nw);
    for(int i=1;i<=x;i++){
        fill(aa),pour(aa,nw);
        aa=copy(nw);
    }
    return aa;
}

请注意,这份代码最后没有把瓶子填满。

下面是数据点 \(5\) 的代码:

int *split(int x,int maxn=16){
    int zero=make(0);
    x=copy(x);fill(x);
    int *res=new int[K];
    for(int i=31;i>maxn;i--) res[i]=zero;
    for(int i=maxn;i>=0;i--){
        int a=make((1<<i)-1),b=make(1);
        int nw=copy(x);fill(nw);
        pour(nw,a),pour(nw,b),res[i]=b;
        b=lft(b,i),fill(b),x=del(x,b);
    }
    return res;
}

这个数据点有个小细节,根据范围二进制只有后 \(16\) 位有值,但是这个函数是后面的关键函数,建议传一个开始处理的位置,也就是代码里的 \(maxn\)

\(6.a\times b\)

不难想到先把 \(a\) 二进制分解,如果第 \(i\) 位有值把答案加上 \(b\times 2^i\) 即可,判断第 \(i\) 位有没有值还是之前的套路,把 \(a_i\) 左移很多位,如果有值容积会很大,直接把 \(b\times 2^i\) 倒进去。

关于处理 \(b\times 2^i\),可以从低位向高位枚举,没经过以为把 \(b\) 左移一位(左移操作再上面)。

int mul(int x,int y,int maxn=16,int siz=16){
    int nw=make(INF);
    int *a=split(x,maxn);
    y=copy(y),fill(y);
    for(int i=0;i<=siz;i++){
        a[i]=lft(a[i],24);
        pour(y,a[i]),fill(y),pour(a[i],nw);
        y=lft(y,1),fill(y);
    }
    return nw;
}

\(7.a\oplus b\)

这个点纯送分,用数据点 \(5\)\(a,b\) 二进制拆分后,每一问用数据点 \(2\) 相减算绝对值即可。

注意不要输出二进制以后的一串瓶子,最后记得化成十进制数。

int _xor(int x,int y,int maxn=16){
    int zero=make(0);
    int *res=new int[K],*a=split(x),*b=split(y);
    for(int i=maxn;i>=0;i--){
        res[i]=del(a[i],b[i]);
    }
    int nw=make(INF);
    for(int i=maxn;i>=0;i--){
        res[i]=lft(res[i],i),fill(res[i]);
        pour(res[i],nw);
    }
    return nw;
}

$8.\left \lfloor \frac{a}{10} \right \rfloor $

如果想骗分的话用数据点 \(5\) 实现的:

if(a>=b) a-=x;

一个个减可以获得 \(8\) 分。

我们需要完成一个带余除法(余数后面有用),基础逻辑还是倍增,首先预处理 \(y\times 2^i\),循环里算步数太多,然后还是用数据点 \(5\) 实现的那个条件语句直接算就行了。

int chu(int x,int y,int opt,int maxn=16){
    int nw=make(INF);
    int *res=new int[K];
    res[0]=make(y),fill(res[0]);
    for(int i=1;i<=maxn;i++) res[i]=lft(res[i-1],1),fill(res[i]);
    for(int i=maxn;i>=0;i--){
        //cout<<"i: "<<i<<"\n";
        int a,e=make(1),fx=copy(x);fill(fx);
        pour(res[i],e),empty(e),a=copy(res[i]);
        pour(fx,a),pour(fx,e),e=lft(e,i),fill(e),pour(e,nw),fill(e),e=lft(e,maxn-i+1);
        fill(res[i]),pour(res[i],e),x=del(x,e);
    }
    if(opt==1) return nw;
    else return x;
}

代码里 \(opt=1\) 算的是商,\(opt=2\) 算的是余数。

\(9.a\times b \bmod 2^{18}\)

\(262144=2^{18}\) 应该都能看出来吧,这个数据点的主要难度在于值域太大,直接乘超过了上限。

如果把 \(a,b\) 拆成二进制,答案是 \(\sum 2^{i+j}a_ib_j\),如果 \(i+j>18\),那么这对 \(i,j\)是完全没有贡献的。

所以可以把乘法做一点小改动,从低位到高位枚举 \(a\) 的每个二进制位 \(i\),每次把这个时候 \(b\) 中代表原来 \(b_{18-i}\)\(1\) 删掉,之所以说原来的是因为再前面写乘法的时候循环里会不断把 \(b\) 左移,这样操作之后所有使得 \(i+j>18\)\(j\) 都在之前被删掉了。

这样累加答案就不会超过上限了,最后做一次前面实现的带余除法即可。

int mul2(int *a,int y){
    int *b=split(y,18),res=make(INF);
    for(int i=0;i<=18;i++){
        a[i]=lft(a[i],24),b[18-i]=lft(b[18-i],18),fill(b[18-i]),y=del(y,b[18-i]),y=copy(y),fill(y);
        pour(y,a[i]),fill(y),pour(a[i],res),y=lft(y,1),fill(y); 
    }
    return chu(res,(1<<18),2,20);
}

\(10.a^b\)

终于只剩最后一个数据点了,这个数据点只是看起来可怕,实际上就是普通快速幂的逻辑,预处理出 \(a^{2^i}\),把 \(b\) 二进制拆分,如果这一位有值处理方法和数据点 \(6\) 的一样。

int qpow(int x,int y){
    swap(x,y);
    int *b=split(y),res[K],nw=make(INF),e=make(1);
    fill(e),pour(e,nw),res[0]=copy(x),fill(res[0]);
    for(int i=1;i<=5;i++) res[i]=mul(res[i-1],res[i-1]),res[i]=copy(res[i]),fill(res[i]);
    for(int i=5;~i;i--){
        b[i]=lft(b[i],25);
        pour(res[i],b[i]),fill(e);
        int m=minmax(b[i],e).second;
        nw=mul(m,nw,19,19);
    }
    return nw;
}

每个部分的代码都放了,就不放全代码了,影响观感。

posted @ 2025-02-17 16:14  星河倒注  阅读(31)  评论(0)    收藏  举报