zoukankan      html  css  js  c++  java
  • 【数论 dp】2048

    考场上一个DFS优化乱加就对了一个无解的点

    2048

    题目描述

    给定一个长度为 n 的数列,在这个数列中选取一个子序列使得这个子序列中的数能合出2048

    对于合并操作,可以选择这个序列中的任意两个数进行合并,当然这两个数必须是相同的(即2个x合并后成为一个2x)

    对于每个序列,只要进行若干次合并操作后,这个序列中至少有一个2048(可以有其他数剩余),就称这个序列是合法的

    我们可以认为只要选取的数在原数列中的位置不同,这些序列就是不同的

    对于给定的数列,小朋友们需要算出有多少子序列是合法的,并把这个数 对 998244353 取模

    输入格式

    第一行一个正整数n表示数列长度。

    第二行 n 个数 Ai,表示这个数列。

    输出格式

    输出一行,为序列数模 998244353 后的值。

    一些说明与提示

    与原版2048不同,这个数列中的数可以是2048以内的任意正整数。

    998244353=119×2^23+1,与本题的解题无关。

    数据规模与约定

    40%的数据,n20

    70%的数据, n500

    100%的数据, n100000,1Ai2048

    时间限制: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
    ",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
    ",(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
    ",f[t-1][2048]*tem%P);
    65   return 0;
    66 }

    END

  • 相关阅读:
    map的初级应用
    RB-Tree删除详解
    RB-Tree插入过程详解
    红黑树操作详解——很形象的过程
    一个数据结构可视化的神奇网址——形象理解
    关于B树B+树的详细解释——绝对精彩
    c++入门之函数指针和函数对象
    树的平衡之AVL树——错过文末你会后悔,信我
    二叉查找树的删除
    1 vmware 如何联网,以及行命令令初步
  • 原文地址:https://www.cnblogs.com/antiquality/p/8646079.html
Copyright © 2011-2022 走看看