【洛谷6962】[NEERC2017] Knapsack Cryptosystem(Meet in Middle+数论二合一)
- 有一个长度为\(n\)的正整数序列\(a\)满足任意\(a_i>\sum_{j=1}^{i-1}a_j\)且\(\sum_{i=1}^na_i<2^{64}\),它和一个与\(2^{64}\)互质的正整数\(r\)构成私钥。并令\(b_i\equiv a_i\cdot r(mod\ 2^{64})\),它与\(r\)构成了公钥。
- 现有一个\(01\)串\(t\),给你序列\(b\)和\(s=\sum_{i=1}^nt_ib_i(mod\ 2^{64})\),要求还原出\(t\)。
- \(n\le64\)
\(Meet\ in\ Middle\)
暂且不管\(a\)和\(r\)是什么东西,似乎问题就是给我们一个序列\(b\),要求构造一个从序列中选出若干个数的方案,使得它们的和为\(s\)。
这时候容易想到使用\(Meet\ in\ Middle\),但复杂度是\(O(2^{\frac n2})\)的,最多跑一跑\(n\le40\)的数据。
暴力的数论题
注意到私钥\(a\)实际上有一个很好的性质,由于\(a_i>\sum_{j=1}^{i-1}a_j\),因此选出若干个\(a_i\)的和只有唯一的拆解方式:从大往小枚举,每次能减就减(因为任意\(a_i\)都无法替代)。
同时,通过这个性质可以得到:\(2^{64}>\sum_{i=1}^{n}a_{i}>2^{n-1}a_1\)。
也就是说,\(a_1<2^{65-n}\),当\(n>40\)的时候是完全可以暴枚的。
我们先把\(b_1\)分解成\(B\times 2^x\)的形式,因为\(r\)与\(2^{64}\)互质,所以\(a_1\)也必然可以表示成\(A\times 2^x\)的形式。
\(O(2^{65-n-x})\)枚举\(A\)得到\(A\cdot r\equiv B(mod\ 2^{64-x})\),然后便可以得到\(r\)在模\(2^{64-x}\)意义下的逆元\(r^{-1}\equiv A\cdot B^{-1}\)。
而\(r^{-1}\)最前面无法确定的\(x\)位可以继续\(O(2^x)\)枚举,这里总的复杂度是\(O(2^{65-n})\)的。
然后代入检验,用\(b\)还原出\(a\)并判断是否合法,最后尝试对\(s\cdot r^{-1}\)进行分解即可。
P.S. 求模\(2^w\)意义下的逆元有一个简单的方法,直接从低往高枚举二进制下第\(i\)位,如果当前乘积中这位有\(1\),就相当于需要将原数左移\(i\)位与它相加把\(1\)消去,给答案加上\(2^i\)并更新乘积即可。
代码:\(O(2^{\frac n3})\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 64
#define ull unsigned long long
using namespace std;
int n;ull s,b[N+5];
namespace MeetInMiddle
{
map<ull,int> S;
I void dfs1(CI x,Con ull& w=0,Con ull& t=0)//第一遍搜索
{
if(x>n/2) return (void)(S[t]=w);dfs1(x+1,w,t),dfs1(x+1,w|(1<<x-1),t+b[x]);
}
I void dfs2(CI x,Con ull& w=0,Con ull& t=0)//第二遍搜索
{
if(x>n) {if(!S.count(s-t)) return;ull o=w|S[s-t];for(RI i=0;i^n;++i) putchar(48|(o>>i&1));exit(0);}//判断是否能拼起来
dfs2(x+1,w,t),dfs2(x+1,w|(1ull<<x-1),t+b[x]);
}
I void Solve() {dfs1(1),dfs2(n/2+1);}//Meet in Middle
}
namespace Math
{
ull a[N+5];char ans[N+5];
I ull Inv(Con ull& x,CI w) {ull y=x,t=1;for(RI i=1;i^w;++i) y>>i&1&&(t|=1ull<<i,y+=x<<i);return t;}//求模2^w意义下的逆元
I void Solve()
{
RI i,x=0;ull v=b[1];W(!(v&1)) v>>=1,++x;v=Inv(v,64-x);//抠出b[1]中的2
RI k,p=(1<<65-n-x)-1,q=(1<<x)-1;ull o=x?(1ull<<64-x):0,t,f,g,S;for(RI A=1;A<=p;A+=2)//暴枚a[1]除去2的部分
{
for(f=(A*v)&(o-1),k=0;k<=q;++k)//f计算出r逆元的后x位,暴枚r逆元的前x位
{
for(g=f|(k*o),t=a[1]=b[1]*g,i=2;i<=n;++i) if(a[i]=b[i]*g,t>=a[i]||t>t+a[i]) break;else t+=a[i];//用b还原a,同时检验是否合法
if(i>n) {for(S=s*g,i=n;i;--i) ans[i]=48|(S>=a[i]&&(S-=a[i],true));if(!S) puts(ans+1),exit(0);}//合法则尝试分解
}
}
}
}
int main()
{
RI i;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%llu",b+i);scanf("%llu",&s);
return n<=40?MeetInMiddle::Solve():Math::Solve(),0;
}