zoukankan      html  css  js  c++  java
  • 状压DP

    状态压缩 (operatorname{DP}) 是将比较复杂的状态映射成数字后进行 (operatorname{DP})

    难点:设计状态

    基本位运算

    枚举子集

    for(int i=s;i;i=(i-1)&s)
    

    O(1) 计算 int 以内每一个数含有多少个 1

    预处理出 (2^{16}) 次方以内每一个数含有多少 (1)

    int All=(1<<16)-1;
    for(int i=1;i<=All;i++) cnt[i]=cnt[i>>1]+(i&1);
    

    查询的时候将每一个数分为高 (16) 位与低 (16) 为分别查询:

    int Count(int x) { return cnt[x>>16]+cnt[x&All]; }
    

    例题

    P4163 [SCOI2007]排列

    $ exttt{solution}$

    (dp[s][j]) 表示已近选走的数的选择情况,(j) 表示选走的数所组成的数 (mod {d}) 以后的值。

    注意:在转移时不能从直接是

    [dp[s∣(1<<(i-1))][(j*10+num[i]) \%{d}]+=dp[s][j] ]

    需要记录值为 (a[i]) 的数是否已近被转移过了,同一个数铺两次会重复计算。

    核心代码:

    memset(dp,0,sizeof(dp)),dp[0][0]=1;
    for(int s=0;s<(1<<len);s++)
    {
    	 memset(used,0,sizeof(used));
    	 for(int i=1;i<=len;i++)
    	 	 if(!used[num[i]] && !(s & (1<<(i-1))))
    	 	 {
    	 	 	 used[num[i]]=true;
    	 	 	 for(int j=0;j<d;j++) dp[s | (1<<(i-1))][(j*10+num[i])%d]+=dp[s][j];
    			 // k 是第 i 位之前的数组成的数 mod d 的值 
    		 }
    }
    

    P2831 愤怒的小鸟

    $ exttt{solution}$

    (Line[i][j]) 表示以第 (i) 只与第 (j) 只小猪构成的抛物线能射中小猪的集合( (2) 进制)可以通过推公式在预处理 (O(n^3)) 时间内求出:

    [y=ax^2 + bx ]

    推导得:

    [a=dfrac{y_1x_2 - y_2x_1}{x_1x_2(x_1-x_2)} ]

    [b=dfrac{y_2x_1^2-y_1x_2^2}{x_1x_2(x_1-x_2)} ]

    考虑状压:(dp[s]) 表示已经死了的猪的集合状态为 (s) 时最少要发射的鸟数,容易推导出:

    [dp[0]=0 ]

    [dp[s∣line[i][j]]=min{(dp[s]+1)} ]

    [dp[s∣1<<(i-1)]=min{(dp[s]+1)} ]

    然而仅仅这样的话复杂度是 (O(Tn^22^n)) 的,通过不了。。

    优化:

    若令 (x) 为满足 (s & (1<<(x-1))=0) 的最小正整数,则由 (s) 扩展的转移的所有线都要经过 (x)

    原因(例子):先打 ({1,4}) 再打 ({2,3}) 和先打 ({2,3}) 再打 ({1,4}) 不是一样的吗?

    如果这一次转移不打 (x) ,那以后还要再回过头来打 (x) ,这就是多余的转移。

    所以转移时默认 (x) 必选 。

    这样复杂度就是 (O(Tn2^n)) 的了。

    核心代码:

    void solve(double &a,double &b,double a1,double b1,double a2,double b2,double Div)
    {
    	 a=(b1*a2-b2*a1)/Div;
    	 b=(b2*a1*a1-b1*a2*a2)/Div;
    }
    
    memset(cnt,0,sizeof(cnt));
    memset(dp,inf,sizeof(dp));
    dp[0]=0;
    for(int i=1;i<n;i++)
    {
    	 for(int j=i+1;j<=n;j++)
    	 {
    	 	 double a,b;
    	 	 solve(a,b,x[i],y[i],x[j],y[j],x[i]*x[j]*(x[i]-x[j]));
    	 	 if(a>=-zero) continue;
    	 	 for(int k=1;k<=n;k++)
    	 	 {
    	 	 	 double tmp=a*x[k]*x[k]+b*x[k]-y[k];
    	 	 	 if(tmp>=-zero && tmp<=zero) cnt[i][j] |= (1<<(k-1)),cnt[j][i] |= (1<<(k-1));
    		 }
    	 }
    }
    for(int s=0;s<(1<<n);s++)
    {
    	 ll i=1;
    	 while(i<=n && (s & (1<<(i-1)))) i++;
    	 dp[s | (1<<(i-1))]=min(dp[s | (1<<(i-1))],dp[s]+1);
    	 for(int j=i+1;j<=n;j++)
    	 	 dp[s | cnt[i][j]]=min(dp[s | cnt[i][j]],dp[s]+1);
    }
    printf("%d
    ",dp[(1<<n)-1]);
    

    P3959 宝藏

    $ exttt{solution}$

    题目的意思就是找一棵生成树,使得代价和最小。

    状态:设 (dp[s][i]) 为当前生成树已经包含集合 (s) 中的点,并且树高是 (i)

    转移:

    [dp[s][i]=min {(dp[s_0][i-1] + cost)} ]

    (s_0) 满足:(s_0) 能够通过连边成为 (s) 。若要判断一个集合是否是合法的 (s_0) 可以预处理出这个集合所能够连边构成的连通块的集合(即:(Go\_to) 数组),再判断:若

    [(Go\_to[s_0]∣s) == Go\_to[s_0] ]

    则这个集合是合法的。

    考虑 (cost) 怎样计算:设 (ss=s oplus s_0) ,即 (ss) 是在 (s) 但不在 (s_0) 中的元素。(cost) 的计算就是对于每个 (ss) 中的元素取 (s_0) 中的元素向它连一条最短的边求和后 ( imes i)

    核心代码:

    memset(dis,inf,sizeof(dis));
    for(int i=1;i<=m;i++)
    {
    	 u=rd(),v=rd(),d=rd();
    	 dis[u][v]=dis[v][u]=min(dis[u][v],d);
    }
    for(int i=1;i<=n;i++) dis[i][i]=0;
    for(int i=1;i<(1<<n);i++)
    {
    	 for(int j=1;j<=n;j++) if(i & (1<<(j-1)))
    	 	 for(int k=1;k<=n;k++) if(dis[j][k]!=inf) Go_to[i] |= (1<<(k-1));
    }
    memset(dp,inf,sizeof(dp));
    for(int i=1;i<=n;i++) dp[1<<(i-1)][0]=0;
    // 如果直接挖到每个点,那个点的深度是 0 
    for(int s=1;s<(1<<n);s++)
    {
    	 for(int s0=s-1;s0;s0=(s0-1) & s) if((Go_to[s0] | s) == Go_to[s0]) // 通过 s0 加边变为 s 
    	 {
    	 	 int add=0,ss=s^s0; // s 是 s 对于 s0 的补集 
    	 	 for(int i=1;i<=n;i++) if(ss & (1<<(i-1)))
    	 	 {
    	 	 	 int tmp=inf;
    	 	 	 for(int j=1;j<=n;j++) if(s0 & (1<<(j-1))) tmp=min(tmp,dis[i][j]);
    	 	 	 add+=tmp;
    		 }
    		 for(int i=1;i<n;i++) dp[s][i]=min(dp[s][i],dp[s0][i-1]+add*i);
    	 }
    }
    for(int i=0;i<n;i++) ans=min(ans,dp[(1<<n)-1][i]);
    printf("%d
    ",ans);
    

    P2157 [SDOI2009]学校食堂

    $ exttt{solution}$

    状态:设 (f[i][j][k]) 表示第 (1) 个人到第 (i-1) 个人已经打完饭,第 (i) 个人以及后面 (7) 个人是否打饭的状态为 (j) ,当前最后一个打饭的人的编号为 (i+k)(k) 的范围为 (-8)(7) ,所以用数组存时要加上 (8) )。

    转移:

    1. (j&1 = operatorname{True})

    表示第 (i) 个人已经打完饭,(i) 之后的 (7) 个人中,还没打饭的人就再也不会插入到第 (i) 个人前面了。(此时不用消耗时间)

    [f[i+1][j>>1][k-1]=min{(f[i+1][j>>1][k-1],f[i][j][k])} ]

    1. (j&1 = operatorname{False})

    可以把 (i) 以及 (i) 之后的 (7) 个人中选出一个人打饭,也就是枚举选 (0)(7)

    [f[i][j∣(1<<x)][x]=min{(f[i][j∣(1<<x)][x],f[i][j][k]+Time(i+k,i+x))} ]

    其中 (Time(i,j)) 表示如果上一个人编号为 (i) ,当前的人编号为 (j) ,那么做编号为 (j) 的人的菜需要的时间。

    然而,转移还需要考虑到忍耐度的问题。可以被选中的 (x) 在他之前的所有未打饭的人必须能忍受这个人先打饭。

    可以算出到目前为止的 未打饭的人的忍受最大位置 的最小值( (pos) )。对于任何一个人,如果 (i+x > pos) ,就表示他无法满足编号在他之前的所有人,就不要考虑这个人了。

    注(一个奇怪的问题):((a[x]∣a[y]) - (a[x] & a[y]) = a[x] oplus a[y])

    核心代码:

    for(int i=1;i<=n;i++) a[i]=rd(),ren[i]=rd();
    memset(dp,inf,sizeof(dp)),dp[1][0][7]=0;
    for(int i=1;i<=n;i++)
    	 for(int j=0;j<(1<<8);j++)
    	 	 for(int k=-8;k<=7;k++) if(dp[i][j][k+8]!=inf)
    	 	 {
    	 	 	 if(j & 1) dp[i+1][j>>1][k-1+8]=min(dp[i+1][j>>1][k-1+8],dp[i][j][k+8]);
    	 	 	 else
    	 	 	 {
    	 	 	 	 int pos=n;
    	 	 	 	 for(int x=0;x<=7;x++) if(!((j>>x) & 1)) pos=min(pos,i+x+ren[i+x]);
    	 	 	 	 for(int x=0;x<=7;x++) if(!((j>>x) & 1))
    	 	 	 	 {
    	 	 	 	 	 if(i+x>pos) break;
    	 	 	 	 	 dp[i][j | (1<<x)][x+8]=min(dp[i][j | (1<<x)][x+8],dp[i][j][k+8]+((i+k)?(a[i+x] ^ a[i+k]):0));
    				 }
    			 }
    		 }
    for(int i=-8;i<=0;i++) ans=min(ans,dp[n+1][0][i+8]);
    printf("%d
    ",ans);
    

    CF16E Fish

    $ exttt{solution}$

    状态:设 (dp[s]) 表示剩下的鱼的集合。

    转移:枚举一条被吃掉的鱼和一条把它吃掉的鱼,累加到 (dp[s])

    方程:

    [dp[s] = sum_i {sum_j{dp[s | (1<<(j-1))] imes a[i][j] imes P}} ]

    [P = dfrac{1}{leftlfloordfrac{cnt imes (cnt+1)}{2} ight floor} ]

    (operatorname{cnt})(s)(1) 的个数。

    核心代码:

    for(int j=1;j<=n;j++) scanf("%lf",&a[i][j]);
    dp[(1<<n)-1]=1;
    for(int s=(1<<n)-2;s>=0;s--)
    {
    	 int cnt=0,tmp=s;
    	 while(tmp) cnt+=(tmp&1),tmp/=2;
    	 for(int i=1;i<=n;i++)
    	 {
    	 	 if(!(s & (1<<(i-1)))) continue;
    	 	 for(int j=1;j<=n;j++) // i eat j
    	 	 {
    	 	 	 if(s & (1<<(j-1))) continue;
    	 	 	 dp[s]+=dp[s | (1<<(j-1))]*a[i][j]/(cnt*(cnt+1)/2);
    		 }
    	 }
    }
    for(int i=1;i<=n;i++) printf("%.6lf%c",dp[1<<(i-1)],(i==n)?'
    ':' ');
    

    CF165E Compatible Numbers

    $ exttt{solution}$

    这道题写代码不难,关键在于找到递推的方法(DP方程)

    思路:

    如果两个数 (x)(y) 是相容的,那么从 (x) 的二进制里去掉了一些 (1)(x') 一定也能与 (y) 相容。也就是说,如果我们不知道 (a) 与哪个数相容,我们可以尝试把它添上一些 (1) 变成 (a') ,如果 (a') 的答案已知,那么 (a) 的答案就可以直接借用 (a') 的答案。

    比如,我们不知道 (1001) 与哪个数相容,但是知道 (1011)(0100) 相容,那么 (1001) 必定与 (0100) 相容。

    实现:

    状态转移方程:

    [dp[ i ] = dp[i | 1<<j-1 ] ]

    前提是 (dp[i | 1<<j-1 ] e -1)

    初始化(边界): (dpleft[2^{num_i-1} & infty ight] = num[i])

    (infty)((1<<22)-1) (上界)

    核心代码:

    memset(dp,-1,sizeof(dp));
    for(int i=1;i<=n;i++) num[i]=rd(),dp[(~num[i]) & inf]=num[i];
    for(int i=inf;i>=0;i--) if(dp[i]==-1) // 需要转移
    	 for(int j=1;j<=22;j++) if(!(i & (1<<(j-1))) && dp[i | (1<<(j-1))]!=-1) // 可以转移
    	 {
    	 	 dp[i]=dp[i | (1<<(j-1))];
    	 	 break; // 只用转移一次就可以了
    	 }
    for(int i=1;i<=n;i++) printf("%d%c",dp[num[i]],(i==n)?'
    ':' ');
    

    P2150 [NOI2015] 寿司晚宴

    咕咕咕

    XJOI 模拟赛 2021.10.30

    (n) 个数,要求选出最多的数使得选出的数两两互质((n,a_ile 1000))。

    $ exttt{solution}$

    注意到值域的范围比较小,最多 (1)(ge sqrt{1000}=31) 的质因子。

    因此我们将每个数 (le 31) 的质因子状压,留下剩余的大因数。

    我们发现如果剩下的数为 (1),即没有大因数,那么可以非常容易地用状压 dp 完成。

    而加上有大因数的情况后,实际上就变为对于每个含有这个大因数的数字,最多只能选择一个作为答案。

    因此这就转化为了一个类似于背包的状压 dp,滚动一维后暴力转移即可。

    #define Maxn 1005
    #define Maxpown 5005
    #define pb push_back
    int T,n,m,ans,All;
    int a[Maxn],dp[Maxpown],tmp[Maxpown];
    vector<int> Left[Maxn];
    int prime[11]={2,3,5,7,11,13,17,19,23,29,31};
    int main()
    {
    	 T=rd(),All=(1<<11)-1;
    	 while(T--)
    	 {
    	 	 n=rd(),ans=0;
    	 	 for(int i=1;i<=1000;i++) Left[i].clear();
    	 	 for(int i=1,Now;i<=n;i++)
    	 	 {
    	 	 	 a[i]=rd(),Now=0;
    	 	 	 for(int j=0;j<11;j++)
    	 	 	 	 while(a[i]%prime[j]==0) Now|=1<<j,a[i]/=prime[j];
    	 	 	 Left[a[i]].pb(Now);
    		 }
    		 memset(dp,-inf,sizeof(dp)),dp[0]=0;
    		 for(int i:Left[1])
    		 {
    		 	 memset(tmp,-inf,sizeof(tmp));
    		 	 for(int s=0;s<=All;s++) if(!(s & i))
    		 	 	 tmp[s | i]=max(tmp[s | i],dp[s]+1);
    		 	 for(int j=0;j<=All;j++) dp[j]=max(dp[j],tmp[j]);
    		 }
    		 for(int i=2;i<=1000;i++) if(Left[i].size())
    		 {
    		 	 memset(tmp,-inf,sizeof(tmp));
    		 	 for(int j:Left[i])
    		 	 	 for(int s=0;s<=All;s++) if(!(s & j))
    		 	 	 	 tmp[s | j]=max(tmp[s | j],dp[s]+1);
    		 	 for(int j=0;j<=All;j++) dp[j]=max(dp[j],tmp[j]);
    		 }
    		 for(int i=0;i<=All;i++) ans=max(ans,dp[i]);
    		 printf("%d
    ",ans);
    	 }
    	 return 0;
    }
    

    P2473 [SCOI2008] 奖励关

    $ exttt{solution}$

    如果我们假设 (dp(i,s)) 表示进行了 (i) 轮,当前选择过的物品集合为 (s) 的价值期望,会发现这样转移很难维护。

    因为我们的转移一定是从一个集合转移到另一个集合,最终答案为第一轮,没有去物品,这让我们想到要倒过来求。

    我们在转移的时候方便求出这一次增加的价值,那就不妨这样假设:设 (dp(i,s)) 表示在 ([1,i-1]) 轮中选择了集合 (s),在 ([i,k]) 轮中获得价值的期望。

    这样就可以枚举每一个点是否能够去来转移了。

    注意:期望是倒推的,概率是顺推的,而期望和概率都是相加后再除去的。

    #define Maxn 20
    #define Maxk 105
    #define Maxsta 100005
    int k,n,All;
    int a[Maxn],pre[Maxn];
    double dp[Maxk][Maxsta];
    int main()
    {
    	 k=rd(),n=rd(),All=(1<<n)-1;
    	 for(int i=1;i<=n;i++)
    	 {
    	 	 a[i]=rd();
    	 	 for(int tmp=rd();tmp;tmp=rd()) pre[i]|=(1<<(tmp-1));
    	 }
    	 for(int i=k;i>=1;i--)
    	 	 for(int s=0;s<=All;s++)
    	 	 {
    	 	 	 for(int j=1;j<=n;j++)
    	 	 	 {
    	 	 	 	 if((pre[j] & s) != pre[j]) dp[i][s]+=dp[i+1][s];
    				 else dp[i][s]+=fmax(dp[i+1][s],dp[i+1][s | (1<<(j-1))]+a[j]);
    			 }
    			 dp[i][s]/=1.0*n;
    		 }
    	 printf("%.6lf
    ",dp[1][0]);
    	 return 0;
    }
    
  • 相关阅读:
    word上怎么打钩
    POI操作excel常用方法总结
    web.xml中Filter过滤器标签说明
    IDEA使用操作说明(自己总结)
    windows 64位上oracle 11g安装
    List<T>与List<?>的区别
    java分页之假分页
    CDN之Web Cache
    HTTP之缓存技术
    HTTP之Cookie和Session
  • 原文地址:https://www.cnblogs.com/EricQian/p/15067413.html
Copyright © 2011-2022 走看看