题解:P6678 [COCI 2019/2020 #2] Popcount

题解:P6678 [COCI 2019 / 2020 #2] Popcount

题目大意

你需要制定一个操作序列,可以包含加,减,按位与,按位或,左移,右移六种运算符。要求对一个非负数 AAA 执行操作序列让 AAA 变为 popcount(A)⁡\operatorname{popcount(A)}popcount(A),即 A\texttt{A}A 在二进制下的 1 的个数。这个非负整数的范围是 [0,2n−1][0,2^n-1][0,2n1]。操作序列的每条操作形如 A=<expr>\texttt{A=<expr>}A=<expr>,其中 <expr>\texttt{<expr>}<expr> 可以为 A\texttt{A}A<num>\texttt{<num>}<num>,一个非负十进制整数,或 <expr><operator><expr>\texttt{<expr><operator><expr>}<expr><operator><expr>,其中 <operator>\texttt{<operator>}<operator> 为上文提到的六种运算符之一。

<expr>\texttt{<expr>}<expr> 中出现 A\texttt{A}A 的次数不能超过 5 次每条操作不能超过 10310^3103 个字符。操作序列数不超过 kkk

Subtask1

注:作者为了表示简洁下文中的操作可能不按题目要求写,注意提交时要保证所有运算都加上括号,作者在文中可能会省略。

由于 k=n−1k=n-1k=n1,所以计算 popcount⁡\operatorname{popcount}popcount 可以直接枚举 AAA 的每一位,即 A=A-(A&(1<<i))+(A&(1<<i)>>i)\texttt{A=A-(A\&(1<<i))+(A\&(1<<i)>>i)}A=A-(A&(1<<i))+(A&(1<<i)>>i),相当于减去这一位再加上这一位的贡献,让 iii 从小到大枚举答案就不会与未统计的位发生冲突。

Subtask1 代码

	if(k>=n-1){
		cout<<n-1<<'\n';
		for(int i=1;i<n;i++){
			cout<<"A=((A-(A&(1<<"<<i<<")))+((A&(1<<"<<i<<"))>>"<<i<<"))\n";
		}
	}

Subtask2

注意到 Subtask1 中一次操作只能统计一位的贡献,效率很低,考虑如何一次操作多统计几位。

根据 Subtask1 的思路,相当于先将一位从 AAA 中减掉再加上这一位的贡献,其中减去一位可以优化为 A&((0-1)-(1<<i))\texttt{A\&((0-1)-(1<<i))}A&((0-1)-(1<<i)),只用了一个 AAA,那么后面就可以有 4 个 AAA,一次统计 4 个数的贡献,即 A&((0-1)-(1<<i)-(1<<i+1)-(1<<i+2)-(1<<i+3))+\texttt{A\&((0-1)-(1<<i)-(1<<i+1)-(1<<i+2)-(1<<i+3))+}A&((0-1)-(1<<i)-(1<<i+1)-(1<<i+2)-(1<<i+3))+
((A>>i)&1)+((A>>i+1)&1)+((A>>i+2)&1)+((A>>i+3)&1)\texttt{((A>>i)\&1)+((A>>i+1)\&1)+((A>>i+2)\&1)+((A>>i+3)\&1)}((A>>i)&1)+((A>>i+1)&1)+((A>>i+2)&1)+((A>>i+3)&1)

Subtask2 代码

	if(n==500&&k==128){
		cout<<125<<'\n';
		for(int i=1;i<n;i+=4){
			cout<<"A=(((A&(((((0-1)-(1<<"<<i<<"))-(1<<"<<i+1<<"))-(1<<"<<i+2<<"))-(1<<"<<i+3<<")))+((A>>"<<i<<")&1))+((((A>>"<<i+1<<")&1)+((A>>"<<i+2<<")&1))+((A>>"<<i+3<<")&1)))\n";
		}
	}

Subtask3

现在题目要求 k≤log⁡2nk \le \log_2 nklog2n,可以运用类似线段树的思想:

先把 AAA 的二进制数位划分成若干给长度为 202^020 的段,此时单看每个段内的数就是每个段内的答案,然后将相邻的段合并为长度为 212^121 的段,其中的数即为原本的两个段的答案之和,然后以此类推,直到合并到段的长度大于等于 nnn 就是答案。

具体的写法就是先将当前的 AAA 分成若干个长为 2i2_i2i 的段,并将其交替分为两个部分,再让靠后的部分右移 2i2^i2i 位,即与对应的前一个段重合,再相加就是答案。

变成操作就是 (A&(1<<0+1<<1+1<<4))+(A&(1<<2+1<<3))>>(1>>0)\texttt{(A\&(1<<0+1<<1+1<<4))+(A\&(1<<2+1<<3))>>(1>>0)}(A&(1<<0+1<<1+1<<4))+(A&(1<<2+1<<3))>>(1>>0)(以 n=5n=5n=5,段长为 212^121 为例)。

Subtask3 代码

	else if(k==7){
		int pp=1,cnt=0;
		while(pp<n){
			cnt++;pp<<=1;
		}
		cout<<cnt<<'\n';
		int len=1,p=0;
		for(int i=1;len<n;i++){
			cout<<"A=((A&";
			vector<int> v;
			int u=0;
			while(u<n){
				for(int j=0;j<len;j++){
					v.push_back(u);u++;
					if(u==n)break;
				}
				u+=len;
			}
			for(int j=1;j<v.size();j++)cout<<"(";
			for(int j=0;j<v.size();j++){
				cout<<"(1<<"<<v[j]<<")";
				if(j)cout<<")";
				if(j<v.size()-1)cout<<"+";
			}
			cout<<")+((A&";
			v.clear();
			u=len;
			while(u<n){
				for(int j=0;j<len;j++){
					v.push_back(u);u++;
					if(u==n)break;
				}
				u+=len;
			}
			for(int j=1;j<v.size();j++)cout<<"(";
			for(int j=0;j<v.size();j++){
				cout<<"(1<<"<<v[j]<<")";
				if(j)cout<<")";
				if(j<v.size()-1)cout<<"+";
			}
			cout<<")>>(1<<"<<p<<")))\n";
			len<<=1;p++;
		}
	}

Subtask4

看着 100≤n≤500,k=10100 \le n \le 500,k=10100n500,k=10 可以用 Subtask3 的方法过,但题目要求一条操作长度不能超过 10310^3103 个字符,显然会超。

注意到题目并没有限制 <num>\texttt{<num>}<num> 的大小,所以我们可以把原本的 (1<<x)+(1<<y)+(1<<z)...\texttt{(1<<x)+(1<<y)+(1<<z)...}(1<<x)+(1<<y)+(1<<z)... 用数字表示就不会过长了,不过要写高精度。

完整代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e9;
struct N{
	ll a[110],n;
	N(){
		memset(a,0,sizeof(a));
		n=0;
	}
};
N cheng(N a,ll v){
	N b;
	for(int i=1;i<=a.n;i++){
		b.a[i]=a.a[i]*v;
	}
	b.n=a.n;
	for(int i=1;i<=b.n;i++){
		b.a[i+1]+=b.a[i]/mod;
		b.a[i]%=mod;
		if(i==b.n&&b.a[i+1])b.n++;
	}
	return b;
}
N jia(N a,N b){
	N c;
	c.n=max(a.n,b.n);
	for(int i=1;i<=c.n;i++){
		c.a[i]=a.a[i]+b.a[i];
	}
	for(int i=1;i<=c.n;i++){
		c.a[i+1]+=c.a[i]/mod;
		c.a[i]%=mod;
		if(c.a[i+1]&&i==c.n)c.n++; 
	}
	return c;
}
N pow(ll b){
	N a;
	a.n=1;a.a[1]=1;
	while(b--)a=cheng(a,2);
	return a;
}
void pr(N a){
	for(int j=a.n;j;j--){
		int len=to_string(a.a[j]).size();
		while(j!=a.n&&len<9){
			cout<<0;len++;
		}
		cout<<a.a[j];
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	int n,k;
	cin>>n>>k;
	if(n==1){
		cout<<"0\n";
		return 0;
	}
	if(k>=n-1){//Subtask1
		cout<<n-1<<'\n';
		for(int i=1;i<n;i++){
			cout<<"A=((A-(A&(1<<"<<i<<")))+((A&(1<<"<<i<<"))>>"<<i<<"))\n";
		}
	}
	else if(n==500&&k==128){//Subtask2
		cout<<125<<'\n';
		for(int i=1;i<n;i+=4){
			cout<<"A=(((A&(((((0-1)-(1<<"<<i<<"))-(1<<"<<i+1<<"))-(1<<"<<i+2<<"))-(1<<"<<i+3<<")))+((A>>"<<i<<")&1))+((((A>>"<<i+1<<")&1)+((A>>"<<i+2<<")&1))+((A>>"<<i+3<<")&1)))\n";
		}
	}
	else if(k==7){//Subtask3
		int pp=1,cnt=0;
		while(pp<n){
			cnt++;pp<<=1;
		}
		cout<<cnt<<'\n';
		int len=1,p=0;
		for(int i=1;len<n;i++){
			cout<<"A=((A&";
			vector<int> v;
			int u=0;
			while(u<n){
				for(int j=0;j<len;j++){
					v.push_back(u);u++;
					if(u==n)break;
				}
				u+=len;
			}
			for(int j=1;j<v.size();j++)cout<<"(";
			for(int j=0;j<v.size();j++){
				cout<<"(1<<"<<v[j]<<")";
				if(j)cout<<")";
				if(j<v.size()-1)cout<<"+";
			}
			cout<<")+((A&";
			v.clear();
			u=len;
			while(u<n){
				for(int j=0;j<len;j++){
					v.push_back(u);u++;
					if(u==n)break;
				}
				u+=len;
			}
			for(int j=1;j<v.size();j++)cout<<"(";
			for(int j=0;j<v.size();j++){
				cout<<"(1<<"<<v[j]<<")";
				if(j)cout<<")";
				if(j<v.size()-1)cout<<"+";
			}
			cout<<")>>(1<<"<<p<<")))\n";
			len<<=1;p++;
		}
	}
	else{//Subtask4
		int pp=1,cnt=0;
		while(pp<n){
			cnt++;pp<<=1;
		}
		cout<<cnt<<'\n';
		int len=1,p=0;
		for(int i=1;len<n;i++){
			cout<<"A=((A&";
			vector<int> v;
			int u=0;
			N s;
			while(u<n){
				for(int j=0;j<len;j++){
					s=jia(s,pow(u));u++;
					if(u==n)break;
				}
				u+=len;
			}
			pr(s);
			cout<<")+((A&";
			v.clear();
			memset(s.a,0,sizeof(s.a));
			s.n=0;
			u=len;
			while(u<n){
				for(int j=0;j<len;j++){
					s=jia(s,pow(u));u++;
					if(u==n)break;
				}
				u+=len;
			}
			pr(s);
			cout<<")>>(1<<"<<p<<")))\n";
			len<<=1;p++;
		}
	}
	return 0;
}

话说这题 SPJ 啥时候修

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值