状压DP十六连测

版权声明:本文为Cooevjnz原创文章,未经Cooevjnz允许也可以转载! https://blog.csdn.net/JacaJava/article/details/81843178

DP应该先学插头
状压DP十六连

A[寿司晚宴]

开始想到了反演,设 f ( d ) 表示两边选出来的公因数为d的方案数,设 F ( d )
好的介绍一个套路,按照最大的质因子分类(没错这不是洲阁筛)
我们发现小于 500 的质数只有8个,可以搞一个 2 16 的状态表示一种方案
让后,我们设 T ( x ) 表示x的最大质因子并将2~n按照T排序
对于这些T相同的数,它们不能被两个人同时拥有,所以分开dp即可
注意要用滚动数组a

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n,w[10000],t,vis[10000],a[10000];
int b[10000],s[10000],M,f[311][311],g[2][311][311];
inline bool c1(int i,int j){ return b[i]==b[j]?i<j:b[i]<b[j]; }
int main(){
    for(int i=2;i<=500;++i){
        if(!vis[i]) w[++t]=i;
        for(int j=1;j<=t && i*w[j]<=500;++j){
            vis[i*w[j]]=1;
            if(i%w[j]==0) break; 
        }
    }
    for(int i=1;w[i]<20;++i)
        for(int j=w[i];j<=500;j+=w[i]) s[j]|=(1<<i-1);
    for(int i=9;i<=t;++i)
        for(int j=w[i];j<=500;j+=w[i]) b[j]=i;
    scanf("%d%d",&n,&M); --n;
    for(int i=1;i<=n;++i) a[i]=i+1;
    sort(a+1,a+1+n,c1);
    f[0][0]=1;
    for(int i=1,j=1;i<=n;i=j){
        while(j<=n && b[a[i]]==b[a[j]]) ++j;
        if(!b[a[i]]){
            for(;i<j;++i){
                for(int S=0;S<256;++S)
                    for(int T=0;T<256;++T)
                        if(f[S][T]){
                            int _S=S|s[a[i]],_T=T|s[a[i]];
                            if(!(S&_T)) g[0][S][_T]=(g[0][S][_T]+f[S][T])%M;
                            if(!(_S&T)) g[0][_S][T]=(g[0][_S][T]+f[S][T])%M;
                            g[0][S][T]=(g[0][S][T]+f[S][T])%M;
                        }
                for(int S=0;S<256;++S)
                for(int T=0;T<256;++T) f[S][T]=g[0][S][T];
                memset(g,0,sizeof g);
            }
        } else {
            memcpy(g[0],f,sizeof f);
            memcpy(g[1],f,sizeof f);
            for(;i<j;++i){
                for(int S=255;~S;--S)
                    for(int T=255;~T;--T){
                        int _S=S|s[a[i]],_T=T|s[a[i]];
                        if(!(_S&T)) g[0][_S][T]=(g[0][_S][T]+g[0][S][T])%M;
                        if(!(S&_T)) g[1][S][_T]=(g[1][S][_T]+g[1][S][T])%M;
                    }
            }
            for(int S=0;S<256;++S)
            for(int T=0;T<256;++T) f[S][T]=((long long)g[0][S][T]+g[1][S][T]-f[S][T]+M)%M;
        }
    }
    int A=0;                
    for(int S=0;S<256;++S)
        for(int T=0;T<256;++T) if(!(S&T)) A=(A+f[S][T])%M;
    printf("%d\n",A);
}



B[Bill的挑战]

并不是字符串题,也不是FFT题
f i , S 表示匹配到了i位,匹配的串为S的方案数
转移的时候直接搞常数很大会T,所以预处理所有字符串在第i位和字符j的匹配情况

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define M 1000003
#define LL long long
using namespace std;
int n,m,k,f[55][1<<16],c[1<<16],t[60][27];
char s[20][100];
int _18520(){
    scanf("%d%d",&m,&k);
    memset(f,0,sizeof f); memset(t,0,sizeof t);
    for(int i=0;i<m;++i) scanf("%s",s[i]);
    n=strlen(s[0]);
    f[0][(1<<m)-1]=1;
    for(int i=0;i<n;++i)
        for(int j=0;j<m;++j)
            if(s[j][i]=='?') for(int k=0;k<26;++k) t[i][k]|=(1<<j);
            else t[i][s[j][i]-'a']|=(1<<j);
    for(int i=0;i<n;++i)
        for(int S=0;S<(1<<m);++S) if(f[i][S])
            for(int j=0;j<26;++j) (f[i+1][S&t[i][j]]+=f[i][S])%=M;
    LL A=0;
    for(int S=0;S<(1<<m);++S) if(c[S]==k) A=(A+f[n][S])%M;
    printf("%d\n",A);
}
int main(){
    for(int i=1;i<1<<15;++i) c[i]=c[i>>1]+(i&1);
    int T; for(scanf("%d",&T);T--;_18520());
}



C[奖励关]

我艹这个期望dp贼鸡儿难
我们设,现在到了第i关,手上已经有了物品集合S,之后最多还能得到的答案为 f i , S
首先答案肯定是 f 1 , ,考虑转移
枚举本关给出的物品j,我们有两种选择拿或者不拿,就有

f i , S = j = 1 n m a x ( f i + 1 , S , f i + 1 , S j + v a l u e j ) , S p r e j n

至于为什么不可以正着做,很简单,不知道上一个状态是否合法

#pragma GCC optimize("O3")
#pragma G++ optimize("O3")
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n,m,v[100],s[100];
double f[100][1<<17];
int main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;++i){
        scanf("%d",v+i);
        for(int x;;s[i]|=(1<<x-1)){
            scanf("%d",&x); if(!x) break;
        }
    }
    for(int i=m;i;--i)
        for(int S=0;S<(1<<n);++S){
            for(int j=1;j<=n;++j)
                if((S&s[j])==s[j]) f[i][S]+=max(f[i+1][S],f[i+1][S|(1<<j-1)]+v[j]);
                else f[i][S]+=f[i+1][S];
            f[i][S]/=n;
        }
    printf("%.6lf\n",f[1][0]);
}



D[一双木棋]

和上面题一样,考虑倒推
设当前局面为S,记 f ( S ) 表示由这个局面开始两者分数之差的最大值
那么如果是A操作,肯定有转移 f ( S ) = m a x ( f ( S ) + v a l u e x )
否则就是 f ( S ) = m i n ( f ( S ) v a l u e y )
这里讲一下怎么表示状态,有两种方法
1.记录每一行填了多少个,那就是一个10位的11进制数,需要用map,由于限制,总状态数很少
2.将已经填满的格子的轮廓线用二进制表示
第一种比较好写

#include<map>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long
using namespace std;
map<LL,int> f; LL bas[20];
int n,m,a[12][12],b[12][12];
inline int dp(LL S){
    if(f.count(S)) return f[S];
    int t[20]={0},c,o=0,r; LL T=S; t[0]=m;
    for(c=0;S;S/=11){
        t[++c]=S%11;
        o^=t[c]&1;
    }
    r=o?1e9:-1e9;
    for(int i=1;i<=n;++i)
        if(t[i]<t[i-1]){
            if(o) r=min(r,dp(T+bas[i])-b[i][t[i]+1]);
            else r=max(r,dp(T+bas[i])+a[i][t[i]+1]);
        }
    return f[T]=r;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j) scanf("%d",a[i]+j);
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j) scanf("%d",b[i]+j);
    bas[1]=1;
    for(int i=2;i<=n;++i) bas[i]=bas[i-1]*11;
    for(int i=1;i<=n;++i) *bas+=m*bas[i];
    f[*bas]=0;
    printf("%d\n",dp(0));
}



E[Vladik and cards]

直接设计状态非常不好,因为需要记录当前数及其出现次数,复杂度爆炸
我们假设已经知道了每个数至少出现L次,那么每个数只可能出现至多L+1次
f i , S 表示已经选了数的集合为S,到了第i位的最大答案
转移的时候,枚举加入的数k,一次性加入L个k或者L+1个k,这里需要预处理每种数的位置方便二分找下一个状态
最后在外层二分L,判断是否有合法的答案即可

#include<stdio.h>
#include<string.h>
#include<algorithm> 
using namespace std;
int n,w[8][1010],c[8],f[256][1010];
inline void gmx(int&x,int y){ x<y?x=y:0; }
inline int ok(int L){
    memset(f,0,sizeof f);
    f[0][0]=1; int ans=-1;
    for(int i=0;i<=n;++i)
        for(int S=0;S<256;++S)
            if(f[S][i]){
                for(int k=0;k<8;++k) if(~S&(1<<k)){
                    int u=lower_bound(w[k]+1,w[k]+c[k]+1,i)-w[k]+L-1;
                    if(u>c[k]) continue;
                    gmx(f[S|(1<<k)][w[k][u]],f[S][i]+L);
                    if(++u>c[k]) continue;
                    gmx(f[S|(1<<k)][w[k][u]],f[S][i]+1+L);
                }
            if(S==255) ans=max(ans,f[S][i]-1);
        }
    if(~ans) return ans; return 0;
}
int main(){ 
    scanf("%d",&n);
    for(int x,i=1;i<=n;++i){
        scanf("%d",&x); --x;
        w[x][++c[x]]=i;
    }
    int l=0,r=n>>3;
    for(int m;l<r;){
        m=l+r+1>>1;
        if(ok(m)) l=m;
        else r=m-1;
    }
    printf("%d\n",ok(l));
}



F[学校食堂Dining]

注意到 b i 很小,考虑从这里入手
设计状态为 f i , S , l a s t 表示1~i-1这些人都拿到了饭时,S表示i~i+7这些人拿饭的情况,last表示上一个拿到饭的人和i的相对位置(可能为负数)
转移,首先就是 f i , S , l a s t f i + 1 , S >> 1 , l a s t 1 , 这个是当i已经拿到饭的情况
否则,枚举下一个拿到饭的人转移,注意考虑后面人的忍耐度即可

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int f[1010][1<<8][20],n,m,t[1010],b[1010];
inline void g(int& x,int y){ (x>y||!x)?x=y:0; }
inline int val(int i,int j){ return i?t[i]^t[j]:0; }
int _18520(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i) scanf("%d%d",t+i,b+i);
    memset(f,0,sizeof f); f[1][0][7]=1;
    for(int i=1;i<=n;++i)
        for(int S=0;S<(1<<8);++S)
            for(int j=-8;j<8;++j) if(f[i][S][j+8]){
                if(S&1) g(f[i+1][S>>1][j+7],f[i][S][j+8]);
                else{
                    int r=n;
                    for(int k=0;k<8;++k)
                        if(!(S&(1<<k))){
                            if(i+k>r) break;
                            r=min(r,i+k+b[i+k]);
                            g(f[i][S|(1<<k)][k+8],f[i][S][j+8]+val(i+j,i+k));
                        }
                }
            }
    int A=1<<29;
    for(int j=-8;j;++j) if(f[n+1][0][j+8]) g(A,f[n+1][0][j+8]);
    printf("%d\n",--A);
}
int main(){
    int T;
    for(scanf("%d",&T);T--;_18520());
}



G[围棋]

欢乐的插头dp
首先考虑转移方式,我们可以逐格转移,而不是逐行否则复杂度爆炸
设计一个状态 f i , j , S , t 表示做到了第i行第j格,轮廓线上的状态是S(三进制),当前匹配到了模板的第t位
转移的时候,枚举填入的数字,让后用kmp求出新的t’,注意到S的第j位在下次转移没用了,可以直接覆盖掉
让后很好的TLE了
分析问题:S的状态太多了,有 3 12 > 500000
精简一下S,我们发现不需要记录S具体是什么,而只需要知道S这一位是否可以和模板的第一行完全匹配即可,这样的话,我们改一下状态,设 F i , j , S , t 1 , t 2 表示到了第i行第j格,当前行和模板的第一行匹配到了t1格,和模板的第二行匹配到了t2格,所以只需要只需要S>>j&1和t2=m不同时满足就是一个合法状态了
这样状态数变成了 O ( n m c 2 2 m ) ,空间还是不够就用滚动吧

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long
#define M 1000000007 
using namespace std;
LL f[2][8][8][10000];
int n,m,c,q,p[14],t[2][6],nt[2][10]={0}; char z[10];
inline void ad(LL& x,LL y){ x=(x+y)%M; }
inline LL pow(LL x,LL k,LL s=1){
    for(;k;x=x*x%M,k>>=1) k&1?s=s*x%M:0;
    return s;
}
int _18520(){
    scanf("%s",z);
    for(int i=0;i<c;++i) t[0][i]=z[i]=='X'?0:(z[i]=='B'?1:2);
    scanf("%s",z);
    for(int i=0;i<c;++i) t[1][i]=z[i]=='X'?0:(z[i]=='B'?1:2);
    memset(nt,0,sizeof nt);
    for(int i=1,j;i<c;++i)
        for(j=i;j;) if(t[0][j=nt[0][j]]==t[0][i]){ nt[0][i+1]=j+1; break; }
    for(int i=1,j;i<c;++i)
        for(j=i;j;) if(t[1][j=nt[1][j]]==t[1][i]){ nt[1][i+1]=j+1; break; }
    memset(f,0,sizeof f);
    f[0][0][0][0]=1; int T=1; int AL=(1<<m-c+1);
    for(int i=0;i<n;++i){
        for(int j=0;j<m;++j){
            T^=1; memset(f[T^1],0,sizeof f[T^1]);
            for(int S=0;S<AL;++S)
                for(int x=0;x<=c;++x)
                    for(int y=0;y<=c;++y) if(f[T][x][y][S]){
                        for(int k=0;k<3;++k){
                            int dx=x,dy=y,dS=S;
                            while(dx && (dx>=c || t[0][dx]!=k)) dx=nt[0][dx];
                            if(t[0][dx]==k) ++dx;
                            while(dy && (dy>=c || t[1][dy]!=k)) dy=nt[1][dy];
                            if(t[1][dy]==k) ++dy;
                            if(dy==c&&(S&(1<<j-c+1))) continue;
                            if(dx==c&&!(dS&(1<<j-c+1))) dS|=(1<<j-c+1);
                            if(dx!=c&&(dS&(1<<j-c+1))) dS^=(1<<j-c+1);
//                          printf("%d %d %d\n",dx,dy,S+(((dx==c)<<j-c)-(wj<<j-c)));
                            ad(f[!T][dx][dy][dS],f[T][x][y][S]);
                        }
                    }
        }
        memset(f[T],0,sizeof f[T]);
        for(int S=0;S<AL;++S)
            for(int x=0;x<=c;++x)
                for(int y=0;y<=c;++y)
                    ad(f[T][0][0][S],f[!T][x][y][S]);
        T^=1;
    }
    LL A=pow(3,n*m);;
    for(int S=0;S<AL;++S) ad(A,-f[!T][0][0][S]);
    printf("%lld\n",(A+M)%M);
}
int main(){
    scanf("%d%d%d%d",&n,&m,&c,&q);
    for(int i=*p=1;i<=m;++i) p[i]=p[i-1]*3;
    for(int T=q;T--;_18520());
}



H[Mutation]

讲真不是dp,不过真的很妙,角度新奇
首先我们考虑一下怎么会产生贡献
对于每个字符i,和他后面的所有j字符中最靠前的那个,才有可能产生贡献
条件就是这些中间的字符都被清除,而且i和j没有被清除
搞清楚这个了之后就考虑快速统计答案了,设 F ( S ) 表示清除了S集合的字符,最后的贡献是多少
那么首先枚举每一对可以产生贡献的字符对 ( i , j ) ,设其价值为 v ,i和j之间的字符集合为 S
显然 F ( S ) 会被加上 v ,而 F ( T )   ( T S ) 也有可能被加上 v ,我们先给所有 T S F ( T ) 加上 v
但是如果有 i T 或者 j T 那么这个贡献是没有的,要减去,也就是 T S i T S j 这样的要减去一个 v
最后我们发现对于 T S i j 被减多了一次,还要加回来
整个过程都在差分数组上面完成,最后做一次前缀和就是答案,这里不要忘记了加上去掉某一类数字的花费

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n,k,T,t[26],f[1<<23],v[26][26],s[26]; char c[200010];
int main(){
    scanf("%d%d%d",&n,&k,&T);
    scanf("%s",c+1);
    for(int i=0;i<k;++i) scanf("%d",t+i);
    for(int i=0;i<k;++i)
        for(int j=0;j<k;++j) scanf("%d",v[i]+j);
    for(int i=0;i<k;++i) f[1<<i]=t[i];
    memset(s,-1,sizeof s); int S=0;
    for(int i=1;i<=n;++i){
        int x=c[i]-'A'; S|=(1<<x);
        for(int j=0;j<k;++j)
            if(~s[j]){
                if(!(s[j]&(1<<j)) && !(s[j]&(1<<x))){
                    f[s[j]]+=v[j][x];
                    f[s[j]|(1<<x)]-=v[j][x];
                    f[s[j]|(1<<j)]-=v[j][x];
                    f[s[j]|(1<<x)|(1<<j)]+=v[j][x];
                }
                s[j]|=(1<<x);
            }
        s[x]=0;
    }
    for(int i=0;i<k;++i)
        for(int j=0;j<(1<<k);++j)
            if(j&(1<<i)) f[j]+=f[j^(1<<i)];
    int A=0;
    for(int j=0;j<(1<<k);++j)
        if((j&S)==j && f[j]<=T && (j^S)) A++;
    printf("%d\n",A);
}



I[Formula 1]

终于到了开头提到的插头dp经典题了(如果不会请移步cdq课件)
f i , j , S 表示现在路径填到了第i行第j个格子,S是一个三进制数,对于每一位
如果为0,表示当前位置轮廓线没有插头
如果为1,表示当前位置轮廓线插头是一条路径的起点
如果为2,表示当前位置轮廓线插头是一条路径的终点
(有一点是显然的,每一段路径必然之和轮廓线相交偶数次,这相当于一个括号序列)
那么,我们可以根据第j位和第j+1位有没有插头来确定当前格子填什么
有四种情况
1.j和j+1均无插头,那么创建两个新插头作为一条路径(┏型情况)
2.j有插头,可以填━或者填┓,有两种情况
3.j+1有插头,可以填┃或者┗,也有两种情况
4.j和j+1均有插头,那么只能填┛,但是请注意这有可能将闭合整个路径,所以除非此格子是整个棋盘最后一个非空格子,否则不能闭合另外两条路径
让后注意一下石头的情况,只能是和情况1一样的,否则不合法,剩下暴力转移就可以辣

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long
using namespace std;
int n,m,p[20]; LL f[2][1600000]; char s[20][20],lst[20][20]={0};
int main(){
    scanf("%d%d",&n,&m);
    for(int i=*p=1;i<20;++i) p[i]=p[i-1]*3;
    for(int i=0;i<n;++i) scanf("%s",s[i]);
    for(int i=n-1;i;--i)
        for(int j=m-1;~j;--j) if(s[i][j]=='.'){ lst[i][j]=1; goto next; }
    next:
    f[0][0]=1; int T=1;
    for(int i=0;i<n;++i){
        for(int j=0;j<m;++j){
            T^=1; memset(f[T^1],0,sizeof f[T^1]);
            for(int S=0,_S;S<p[m+1];++S) if(f[T][S]){
                int t[15]={0},w[15],d[15],c=0;
                for(int k=0;k<=m;++k){
                    t[k]=S/p[k]%3;
                    if(t[k]==1) d[++c]=k;
                    else if(t[k]==2){ w[k]=d[c]; w[d[c--]]=k; }
                }
                if(s[i][j]=='*'){
                    if(!t[j] && !t[j+1]) f[!T][S]+=f[T][S];
                    continue;
                }
                if(!t[j] && !t[j+1]){
                    f[!T][S+p[j]+2*p[j+1]]+=f[T][S];
                } else if(!t[j+1]){
                    f[!T][S]+=f[T][S];
                    f[!T][S-p[j]*t[j]+t[j]*p[j+1]]+=f[T][S];
                } else if(!t[j]){
                    f[!T][S]+=f[T][S];
                    f[!T][S-p[j+1]*t[j+1]+t[j+1]*p[j]]+=f[T][S];
                } else {
                    if(t[j]==1 && t[j+1]==2){
                        if(lst[i][j]) f[!T][S-p[j]-2*p[j+1]]+=f[T][S]; continue;
                    }
                    else if(t[j]==2 && t[j+1]==1){ f[!T][S-2*p[j]-p[j+1]]+=f[T][S]; }
                    else if(t[j+1]==1 && t[j]==1){ f[!T][S-p[j]-p[j+1]-p[w[j+1]]]+=f[T][S]; }
                    else if(t[j+1]==2 && t[j]==2){ f[!T][S-2*p[j]-2*p[j+1]+p[w[j]]]+=f[T][S]; }
                }
            }
        }
        memset(f[T],0,sizeof f[T]);
        for(int S=0;S<p[m];++S) f[T][S*3]=f[!T][S];
        T^=1;
    }
    printf("%lld\n",f[T][0]);
}



J[Eat the Trees]

上面题的弱化版
三进制改成二进制记录有没有插头就可以辣
也不用管路径闭合什么的,反正不限条数

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long
using namespace std;
LL f[12][12][1<<12]; int n,m,s[12][12];
int _18520(int T){
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;++i)
        for(int j=0;j<m;++j)
            scanf("%d",s[i]+j);
    memset(f,0,sizeof f); f[0][0][0]=1;
    for(int i=0;i<n;++i){
        for(int j=0;j<m;++j)
            for(int S=0;S<(1<<m+1);++S){
                int l=(1<<m),u=(1<<j);
                if(s[i][j]==0){
                    if((~S&u) && (~S&l)) f[i][j+1][S]+=f[i][j][S];
                } else if((S&l)&&(S&u)){ f[i][j+1][S-u-l]+=f[i][j][S]; }
                else if(S&l){
                    f[i][j+1][S]+=f[i][j][S];
                    f[i][j+1][S-l+u]+=f[i][j][S];
                } else if(S&u){
                    f[i][j+1][S]+=f[i][j][S];
                    f[i][j+1][S-u+l]+=f[i][j][S];
                } else f[i][j+1][S+u+l]+=f[i][j][S];
            }
        for(int S=0;S<(1<<m);++S) f[i+1][0][S]=f[i][m][S];
    }
    printf("Case %d: There are %I64d ways to eat the trees.\n",T,f[n][0][0]);
}
int main(){
    int T; scanf("%d",&T);
    for(int i=1;i<=T;++i) _18520(i);
}



K[Black & White]

我艹这个题真的是坑,辣鸡插头dp
是本联测最难的题,主要问题就是输出方案
还是像I题一样的套路,设 f i , j , S , C 表示到了i,j,轮廓线上面的颜色为C,连通性状态为S
S是类似于一个集合表示法的东西,一个八进制数,拆开每一位代表每个格子所处联通块编号
这个时候就发现状态数爆炸了,无法转移。
无用状态很多,可以换成bfs的方式,每次转移一格搞一个哈希表存有用状态
转移有一个比较关键的地方:如果当前格子可以把他上面那个格子所在的联通块独立出来那么就不能填进去,还有就是2*2的同色块不能有
每个状态记录一下方案,方便输出答案
细节非常的多所以还是建议参考一下别人的code吧

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define M 199999
#define N 210110
using namespace std;
char s[120][120];
int n,m,T,ps[100][N],o[100][N];
struct HASH{
    int h[N],cnt,nt[N],f[N],s[N],c[N];
    inline void clr(){ cnt=0; memset(h,-1,sizeof h); }
    inline void push(int S,int C,int v,int blk,int lst,int opt){
        int H=(S*13+C)%M;
        for(int i=h[H];~i;i=nt[i])
            if(s[i]==S && c[i]==C){ f[i]+=v; return; }
        f[cnt]=v; nt[cnt]=h[H]; s[cnt]=S; c[cnt]=C; h[H]=cnt;
        ps[blk][cnt]=lst; o[blk][cnt]=opt; ++cnt;
    }
} F[2];
inline void df(int i,int j,int o){
    for(int K=0;K<F[T].cnt;++K){
        int S=F[T].s[K],C=F[T].c[K],b[20],c[20],dS=0;
        int u=(i&&((C>>j)&1)==o),l=(j&&((C>>j-1)&1)==o),ul=i&&j&&(C>>m&1)==o;
        if(l&u&ul) continue; dS=S; //2*2判断
        if(i==n-1&&j==m-1&&!u&&!l&&ul)//最后一格特判
            continue;
        for(int t=0;t<m;++t){ c[t]=C>>t&1; b[m-t-1]=dS&7; dS>>=3; }
        if(i && !u){//如果可能将u所在联通块独立
            int c1=0,c2=0;
            for(int t=0;t<m;++t)
                c1+=(b[t]==b[j]),c2+=(c[t]!=o);
            if(c1==1 && (c2>1||i<n-1||j<m-2)) continue;
        }
        if(u&l){//上面和左边联通
            int v=b[j];
            for(int t=0;t<m;++t)
                if(b[t]==v) b[t]=b[j-1];
        }
        if(!u&&l) b[j]=b[j-1];
        if(!u&&!l) b[j]=m;
        if(c[j]) C|=1<<m; else C&=~(1<<m);
        if(o) C|=1<<j; else C&=~(1<<j);
        memset(c,-1,sizeof c);
        for(int t=dS=0,k=-1;t<m;++t)//重标号
            dS=(dS<<3)|(~c[b[t]]?c[b[t]]:c[b[t]]=++k);
        F[!T].push(dS,C,F[T].f[K],i*m+j,K,o);
    }
}
void print(int k){
    for(int i=n-1;~i;i--)
        for(int j=m-1;~j;j--){
            if(o[i*m+j][k]) s[i][j]='#'; else s[i][j]='o';
            k=ps[i*m+j][k];
        }
    for(int i=0;i<n;++i) puts(s[i]);
}
inline void _18520(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;++i) scanf("%s",s[i]);
    F[0].clr(); F[0].push(0,0,1,0,0,0); T=0;
    for(int i=0;i<n;++i){
        for(int j=0;j<m;++j){
            F[!T].clr();
            if(s[i][j]!='#') df(i,j,0);
            if(s[i][j]!='o') df(i,j,1);
            T^=1;
        }
    }
    int res=0,printed=0;
    for(int K=0;K<F[T].cnt;++K){
        int S=F[T].s[K];
        for(int j=0;j<m;++j) if((S>>j*3&7)>1)goto end;
        res+=F[T].f[K];
        printed=K;
        end:;
    }
    printf("%d\n",res);
    if(res) print(printed); puts("");
}
int main(){
    int T;
    for(scanf("%d",&T);T--;_18520());
}



L[Fish]

暴雨过后的彩虹?(这是个flag)
一看就是非常sb的题啊(雾)
f ( S ) 表示S出现的可能性,S为存活鱼的集合
让后?枚举哪条鱼死了就可以了

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define db double
using namespace std;
int n,c[1<<20]; db f[1<<19],p[20][20];
int main(){
    scanf("%d",&n);
    for(int i=0;i<n;++i)
        for(int j=0;j<n;++j)
            scanf("%lf",p[i]+j);
    for(int i=1;i<(1<<n);++i) c[i]=c[i>>1]+(i&1);
    f[(1<<n)-1]=1;
    for(int S=(1<<n)-1;S;--S)
        for(int i=0;i<n;++i)
            for(int j=0;j<n;++j)
                if(i!=j && S&(1<<i) && S&(1<<j))
                    f[S^(1<<j)]+=f[S]*p[i][j]/(c[S]*(c[S]-1)>>1);
    for(int i=0;i<n;++i) printf("%.6lf ",f[1<<i]);
}



M[Traveling Graph]

也不是一个特别困难的题,只是一开始想错了
首先发现是一个一笔画问题,要求要回到起点,可以走重复的边
先跑个floyd求一求最短路
按照小奥的知识,把度数为奇数的点找出来,每次找两个连起来
这里用状压dp来做就可以了,注意不连通的点,如果度数为0就不管否则-1
其实还有更牛逼的一般带权图匹配,复杂度比这个好很多

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define inf (1<<29)
#define LL long long
using namespace std;
int n,m,s[20][20],d[20]; LL f[1<<20],S,A;
inline void g(LL& x,LL y){ x>y?x=y:0; } 
int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;++i)
        for(int j=0;j<n;++j) s[i][j]=inf;
    for(int x,y,c;m--;){
        scanf("%d%d%d",&x,&y,&c); --x; --y; A+=c;
        s[x][y]=s[y][x]=min(s[x][y],c); d[x]++; d[y]++;
    }
    for(int k=0;k<n;++k)
        for(int i=0;i<n;++i)
            for(int j=0;j<n;++j)
                s[i][j]=min(s[i][j],s[i][k]+s[k][j]);
    for(int i=0;i<n;++i) s[i][i]=0;
    for(int i=1;i<n;++i) if(d[i]&&s[0][i]==inf) return 0&puts("-1");
    m=1<<n; for(int i=0;i<m;++i) f[i]=inf; f[0]=0;
    for(int S=0,i;S<m;++S){
        for(i=0;i<n;++i) if((d[i]&1)&&(S&(1<<i))) break;
        if(i>=n) f[S]=0;
        for(i=0;i<n;++i) if((d[i]&1)&&(~S&(1<<i)))
            for(int j=i+1;j<n;++j) if((d[j]&1)&&(~S&(1<<j))&&s[i][j]<inf)
                g(f[S|(1<<i)|(1<<j)],f[S]+s[i][j]);
    }
    if(f[m-1]>=inf) return 0&puts("-1");
    printf("%d\n",A+f[m-1]);
}



N[A Simple Task]

和上面那题差不多了
首先要记录当前环内有哪些元素,和环尾是哪个
因为一个环有多种表示方式,我们强制将环内编号最小的那个点设为环头
每次从环尾更新,如果可以走到环头那么就更新答案
注意,即使规定了环头一个环依然有两种走法,所以最后答案要/2

#include<queue>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long
using namespace std;
struct E{ int S,lst; }t;
struct edge{ int v,nt; } G[1010];
int n,m,h[110],cnt; LL f[1<<20][21]={0},ans=0;
inline void adj(int x,int y){
    G[++cnt]=(edge){y,h[x]}; h[x]=cnt;
    G[++cnt]=(edge){x,h[y]}; h[y]=cnt;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int x,y,i=1;i<=m;++i){
        scanf("%d%d",&x,&y);
        --x; --y; adj(x,y);
    }
    for(int i=0;i<n;++i) f[1<<i][i]=1;
    for(int S=1;S<(1<<n);++S)
        for(int i=0;i<n;++i) if(f[S][i]){
            int y=0; for(int j=0;j<n;++j) if(S&(1<<j)){ y=j; break; }//找环头
            for(int j=h[i];j;j=G[j].nt)
                if(G[j].v==y) ans+=f[S][i];
                else if(G[j].v>y && ~S&(1<<G[j].v)) f[S|(1<<G[j].v)][G[j].v]+=f[S][i];
        }
    printf("%lld\n",ans-m>>1);//去掉长度为2的答案(不合法)
}



O[Square Subsets]

就是问你有多少个子集乘积为平方数
注意到 a i 非常小,所以肯定直接记录质因数的次数mod 2的情况就可以啦
用线性筛预处理每个数的质因数的次数,方便转移

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define LL long long
#define M 1000000007
using namespace std;
int w[100],t,vis[110],f[2][1<<19];
int n,b[110],s[110],c[72],v[100010]; LL pow[100010];
inline void ad(int& x,LL y){ x=(x+y)%M; } 
int main(){
    scanf("%d",&n); *pow=1;
    for(int i=1;i<=n;++i){
        scanf("%d",v+i); ++c[v[i]]; pow[i]=pow[i-1]*2%M;
    }
    for(int i=2;i<=70;++i){
        if(!vis[i]) w[++t]=i;
        for(int j=1,k;j<=t && (k=i*w[j])<=70;++j){
            vis[k]=1;
            if(i%w[j]==0) break;
        }
    }
    for(int i=1;i<=t;++i){
        for(int k=w[i];k<=70;k*=w[i]) 
            for(int j=k;j<=70;j+=k) s[j]^=(1<<i-1);
    }
    f[0][0]=1;
    for(int i=1;i<=70;++i){
        if(c[i]){
            for(int S=0;S<(1<<19);++S)
                if(f[0][S]){
                    ad(f[1][S^s[i]],f[0][S]*pow[c[i]-1]%M);
                    ad(f[1][S],f[0][S]*pow[c[i]-1]%M);
                }
            swap(f[0],f[1]); memset(f[1],0,sizeof f[1]);
        }
    }
    printf("%d\n",(f[0][0]-1+M)%M);
}



P[集合选数]

首先可以注意到,对于两个数a和ka,如果k有除了2和3以外的质因数那么这两个数选不选是互相不影响的
所以我们对于每个 a 0 ( m o d   2 ) , a 0 ( m o d   3 ) 搞一个类似这样的矩阵

(1) a 2 a 4 a 3 a 6 a 12 a 9 a 18 a 36 a

那么问题就变成了在矩阵中取不能相邻的数的方案数,直接存储每一列状态转移就可以了

#include<stdio.h>
#include<string.h>
#include<algorithm>
#define M 1000000001
#define LL long long
using namespace std;
LL A=1,f[20][1<<12];
int n,s[20][20],c[20];
bool vis[1<<22];
inline LL cal(int x){
    memset(s,0,sizeof s);
    memset(f,0,sizeof f);
    s[0][0]=x;
    for(int j=1;;++j){
        vis[s[0][j]=s[0][j-1]*3]=1;
        if(s[0][j]>n){ *c=j; break; }
    }
    int m=0;    
    for(int i=1;;++i){
        vis[s[i][0]=s[i-1][0]<<1]=1;
        if(s[i][0]>n){ m=i; break; }
        for(int j=1;;++j){
            vis[s[i][j]=s[i][j-1]*3]=1;
            if(s[i][j]>n){ c[i]=j; break; }
        }
    }
    for(int S=0;S<(1<<*c);++S) f[0][S]=!(S&(S<<1));
    for(int i=0;i<m;++i)
        for(int S=0;S<(1<<c[i]);++S) if(f[i][S])
            for(int T=0;T<(1<<c[i+1]);++T)
                if(!(S&T) && !(T&(T<<1))) f[i+1][T]=(f[i][S]+f[i+1][T])%M;
    return f[m][0];
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i)
        if(!vis[i]) A=A*cal(i)%M;
    printf("%lld\n",A);
}

猜你喜欢

转载自blog.csdn.net/JacaJava/article/details/81843178