【数论 dp】2048
考场上一个DFS优化乱加就对了一个无解的点
2048
题目描述
给定一个长度为 n 的数列,在这个数列中选取一个子序列使得这个子序列中的数能合出2048
对于合并操作,可以选择这个序列中的任意两个数进行合并,当然这两个数必须是相同的(即2个x合并后成为一个2x)
对于每个序列,只要进行若干次合并操作后,这个序列中至少有一个2048(可以有其他数剩余),就称这个序列是合法的
我们可以认为只要选取的数在原数列中的位置不同,这些序列就是不同的
对于给定的数列,小朋友们需要算出有多少子序列是合法的,并把这个数 对 998244353 取模
输入格式
第一行一个正整数n表示数列长度。
第二行 n 个数 Ai,表示这个数列。
输出格式
输出一行,为序列数模 998244353 后的值。
一些说明与提示
与原版2048不同,这个数列中的数可以是2048以内的任意正整数。
998244353=119×2^23+1,与本题的解题无关。
数据规模与约定
40%的数据,n≤20
70%的数据, n≤500
100%的数据, n≤100000,1≤Ai≤2048
时间限制:1s
空间限制:256MB
题目分析
首先在题意里显然的是,只有2的幂次数能够最终合并出2048。
那么自然先考虑纯粹的dfs。
裸的DFS
枚举出一种方案之后只需要将所有是2的幂的数加起来,如果合法则和必定大于2048(有些单个无法合并的并不造成影响,因为它们既然合并不了(个数为奇数个)那么加上去就不会大于2048)
优化的DFS
1.把a[]从大到小排序,dfs时候先枚举选当前数
2.dfs时如果check()已经合法,那么显然的,后面的x个数无论怎么选都一定符合条件。
可以发现存在这个恒等式,于是直接加上即可(当然要快速幂或者事先打好一个表。要知道直接右移100000位会炸到哪里去都不知道)
3.测试数据会发现,在最后的几个数(通常为0或1)dfs时,会因sum为2046/2047这样的东西被卡很久。那么我们维护对2的幂次维护一个后缀和,加判断 if sum+suffix[now]<2048 return 意即如果后面全部数选上,都还达不到2048那就一定不合法了。
1 #include<bits/stdc++.h> 2 using namespace std; 3 int n,nx,a[100003],tr,tot; 4 long long ans,suf[100003],m2[100003],ss; 5 bool vis[100003]; 6 set<int>f; 7 const int MO = 998244353; 8 int read() 9 { 10 char ch = getchar();int num = 0; 11 while (ch<'0'||ch>'9')ch = getchar(); 12 while (ch>='0'&&ch<='9'){num=num*10+ch-'0';ch=getchar();} 13 return num; 14 } 15 bool cmp(int a, int b) 16 { 17 return a>b; 18 } 19 bool check() //原始的check 20 { 21 tot = 0; 22 int ax[15],x,y,z; 23 memset(ax, 0, sizeof(ax)); 24 register int i; 25 //a1:1 a2:2 a3:4 a4:8 a5:16 a6:32 a7:64 a8:128 a9:256 a10:512 a11:1024 26 for (i=1; i<=n; i++) 27 if (vis[i]) 28 { 29 x = a[i]; 30 if (x==2048)return 1; 31 if (x==1||x==2||x==4||x==8||x==16||x==32||x==64||x==128||x==256||x==512||x==1024){ 32 y = x;z = 1; 33 while (y!=1){y>>=1;++z;} 34 ax[z]++; 35 } 36 } 37 for (i=1; i<=11; i++) 38 ax[i+1] += ax[i]/2; 39 if (ax[12])return 1; 40 return 0; 41 } 42 bool new_check() //略有升级的check 43 { 44 int cnt = 0; 45 for (int i=1; i<=n; i++) 46 if (vis[i] && f.count(a[i])) 47 cnt+=a[i]; 48 return cnt>=2048; 49 } 50 void search(int now, int sum) //check 是O(n)的,浪费很多。直接保存sum每次传递 51 { 52 if (now==n+1) 53 {if (sum>=2048){ans++;if (ans>=MO)ans-=MO;}return;} 54 if (sum>=2048){ 55 ans += m2[n-now+1]; 56 if (ans>=MO)ans-=MO; 57 return; 58 } 59 if (sum+suf[now]<2048)return; 60 vis[now] = 1; 61 if (f.count(a[now])) 62 search(now+1, sum+a[now]); 63 else search(now+1, sum); 64 vis[now] = 0; 65 search(now+1, sum); 66 } 67 int main() 68 { 69 register int i,j; 70 for (int i=1; i<=11; i++) 71 f.insert(1<<i); 72 memset(vis, 0, sizeof(vis)); 73 n = read();nx = 0; 74 for (i=1; i<=n; i++) 75 { 76 tr = read(); 77 a[++nx] = tr; 78 } 79 n = nx; 80 sort(a+1, a+n+1, cmp); 81 m2[1] = 2; 82 for (int i=2; i<=n; i++) 83 m2[i] = m2[i-1]*2%MO; 84 for (i=n; i>=1; i--) 85 if (f.count(a[i])) 86 suf[i] = suf[i+1]+a[i]; 87 search(1, 0); 88 printf("%lld\n",ans); 89 return 0; 90 }
嗯这样优化之后就可以过5个点(不过还有四个点WA,最后的点TLE?这不是幂数模数的问题,玄学)
假的DFS优化
像我这样在读入的时候只读2的幂次数的人已经不多了
数学考虑
既然我们都已经在dfs优化中想到:如何计算一个集合元素为n的所有子集个数。
我们再往dp靠一靠就可以得到一个比标算的代码复杂度还要少的算法了。先贴上:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxx = 2048; 4 const int MO = 998244353; 5 int read() 6 { 7 char ch = getchar();int num; 8 for (num = 0; !isdigit(ch); ch = getchar()); 9 for (; isdigit(ch); ch = getchar())num = (num<<3)+(num<<1)+ch-48; 10 return num; 11 } 12 long long qmi(long long a, long long b)//快速幂 13 { 14 long long ret = 1, xx = a; 15 for(;;) 16 { 17 if (b&1)ret=(xx*ret)%MO; 18 if (b>>=1)xx=(xx*xx)%MO; 19 else break; 20 } 21 return ret; 22 } 23 int n,m,x; 24 long long f[2048],ss; 25 int main() 26 { 27 n = read(); 28 f[0] = 1; 29 for (int i=1; i<=n; i++) 30 { 31 x = read(); 32 if (x & (x-1))continue; //判断是否为2的幂次 33 m++; 34 for (int j=maxx-1; j>=x; j--) 35 f[j] = (f[j]+f[j-x])%MO; 36 } 37 for (int i=0; i<maxx; i++) 38 ss = (f[i]+ss)%MO; 39 printf("%lld\n",(qmi(2, m)-ss+MO)%MO*qmi(2, n-m)%MO); 40 return 0; 41 }
我们先算出有2的幂次数m个
记m个数共可以表示出方案数a
记m个数可表示出小于2048的数的方案为b
显然这m个数可表示出大于等于2048的方案数即a-b,就是上述的 (qmi(2, m)-ss+MO)%MO ,至于在模之前加上MO是因为这个qmi()和ss取模过,故不能保证取模后依然qmi()>ss
后面的 *qmi(2, n-m)%MO 就是乘上对答案无关的方案数。
至此,我们用41行就跑过了这题……
标算
最后贴一发标算
一个子序列或者说子集合法的条件是:只要二的次幂的和 不小于 2048 即可
可以想象,二进制加法即2048合并的过程
转化为用DP统计有多少子集的和不小于 2048 ,很简单的一个背包问题
大概大佬都是这样简洁的“很简单的xx问题”的吧
不过好像现在做完再看这题也“很简单的数学+dp问题”
我对标算的理解都写在注释里了
1 #include<cstdio> 2 #include<cstdlib> 3 #include<algorithm> 4 using namespace std; 5 typedef long long ll; 6 7 inline char nc(){ 8 static char buf[100000],*p1=buf,*p2=buf; 9 if (p1==p2) { p2=(p1=buf)+fread(buf,1,100000,stdin); if (p1==p2) return EOF; } 10 return *p1++; 11 } 12 13 inline void read(int &x){ //指针快读 14 char c=nc(),b=1; 15 for (;!(c>='0' && c<='9');c=nc()) if (c=='-') b=-1; 16 for (x=0;c>='0' && c<='9';x=x*10+c-'0',c=nc()); x*=b; 17 } 18 19 const int P=998244353; //神奇的模数 20 const int N=100005; 21 22 int n,a[N]; 23 ll f[15][2500],sum[15]; 24 int tag[N],cnt[N]; 25 26 ll fac[N],inv[N]; //fac[]阶乘 inv[]逆元 27 inline void Pre(){ //预处理阶乘及逆元(inv[]) 28 fac[0]=1; 29 for (int i=1;i<=n;i++) fac[i]=fac[i-1]*i%P; 30 inv[1]=1; 31 for (int i=2;i<=n;i++) inv[i]=(P-P/i)*inv[P%i]%P; 32 inv[0]=1; 33 for (int i=1;i<=n;i++) (inv[i]*=inv[i-1])%=P; 34 } 35 36 inline ll Pow(ll a,int b){ //快速幂 37 ll ret=1; 38 for (;b;b>>=1,a=a*a%P) if (b&1) ret=ret*a%P; 39 return ret; 40 } 41 42 #define C(n,m) (fac[(n)]*inv[(m)]%P*inv[(n)-(m)]%P) //跑组合数 43 44 int main(){ 45 read(n); Pre(); 46 for (int i=1;i<=n;i++) read(a[i]); 47 for (int i=1;i<=2048;i<<=1) tag[i]=1; //2的幂次 48 int tem=1; 49 for (int i=1;i<=n;i++) 50 tag[a[i]]?cnt[a[i]]++:(tem*=2)%=P; //cnt[] 2的幂次的数的个数 51 f[0][0]=1; sum[0]=1; 52 int t=1; 53 for (int i=1;i<=2048;i<<=1,t++){ //背包转移 54 ll tsum=0; 55 for (int k=0;i*k<=2048 && k<=cnt[i];k++){ 56 int tmp=i*k; // n 57 (tsum+=C(cnt[i],k))%=P; //实际上std似乎没有意识到ΣC(i, n)=2^n? 58 // i=0 59 for (int j=2048;~j;j--) (f[t][min(tmp+j,2048)]+=f[t-1][j]*C(cnt[i],k)%P)%=P; 60 } 61 (f[t][2048]+=(Pow(2,cnt[i])-tsum+P)*sum[t-1]%P)%=P; 62 for (int j=0;j<=2048;j++) (sum[t]+=f[t][j])%=P; 63 } 64 printf("%lld\n",f[t-1][2048]*tem%P); 65 return 0; 66 }
END