挑战程序设计竞赛 3.4 熟练掌握动态规划 POJ 2441:Arrange the Bulls POJ 3254:Corn Fields POJ 2836:Rectangular Covering POJ 1795:DNA Laboratory POJ 3411:Paid Roads POJ 3420:Quad Tiling POJ 3735:Training little cats POJ 3171:Cleaning Shifts

【Summarize】

  1. 通常在状态较小的子集操作的时候要想到状态压缩

  2. 如果最优问题输出方案难以在递推时求出时,可以考虑先求最优值,倒搜求解最优方案

  3. 在子集计算问题中,在预处理出的可能合法子集中求解效率会大大提高

  4. 在稀疏矩阵的乘法中,注意0的特判以减小运算量

/*
    题目大意:每个人有过个喜欢的篮球场地,但是一个场地只能给一个人,
    问所有人都有自己喜欢的场地的方案数。
    题解:状态S表示已经用了那些场地,顺序递推每个人满足需求的情况即可。
*/
#include <cstdio>
#include <cstring>
using namespace std;
const int N=25;
int n,m,u,x,dp[2][1<<20],a[N][N];
int main(){
    while(~scanf("%d%d",&n,&m)){
        memset(a,0,sizeof(a));
        memset(dp,0,sizeof(dp));
        dp[0][0]=1;
        for(int i=1;i<=n;i++){
            scanf("%d",&u);
            while(u--)scanf("%d",&x),a[i][x]=1;
        }
        for(int i=1;i<=n;i++){
            for(int j=0;j<(1<<m);j++)if(dp[1-i&1][j]){
                for(int k=1;k<=m;k++){
                    if(a[i][k]&&j!=(j|1<<(k-1)))dp[i&1][j|1<<(k-1)]+=dp[1-i&1][j];
                }
            }memset(dp[1-i&1],0,sizeof(dp[1-i&1]));
        }int ans=0;
        for(int i=1;i<(1<<m);i++)ans+=dp[n&1][i];
        printf("%d
",ans);
    }return 0;
}

POJ 3254:Corn Fields

/*
    题目大意:给出一个n*m的地图,0表示不能放,问往格子上摆放上下左右不能相邻的棋子有几种方案。
    题解:首先预处理出单行合法的情况和地图中有障碍的状态,
    每次计算一行的方案时,我们枚举两种合法状态,
    如果两种状态可以上下相邻则累加上一行该状态的答案。
    然后累计最后一行每种状态下的答案即可.
*/
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=15,M=1<<N,mod=100000000;
int st[M],mp[M],dp[N][M],n,m,x;
int main(){
    while(~scanf("%d%d",&n,&m)){
        memset(st,0,sizeof(st));
        memset(mp,0,sizeof(mp));
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++){
                scanf("%d",&x);
                if(!x)mp[i]|=(1<<(j-1));
            }
        }int cnt=0;
        for(int i=0;i<(1<<m);i++){
            if(!(i&(i<<1)))st[cnt++]=i;
        }for(int i=0;i<cnt;i++){
            if(!(mp[1]&st[i]))dp[1][i]=1;
        }for(int i=2;i<=n;i++){
            for(int j=0;j<cnt;j++){
                if(mp[i]&st[j])continue;
                for(int u=0;u<cnt;u++){
                    if(mp[i-1]&st[u])continue;
                    if(!(st[j]&st[u]))dp[i][j]=(dp[i][j]+dp[i-1][u])%mod;
                }
            }
        }int ans=0;
        for(int i=0;i<cnt;i++)ans=(ans+dp[n][i])%mod;
        printf("%d
",ans);
    }return 0;
}

POJ 2836:Rectangular Covering

/*
    题目大意:给出二维平面的一些点,现在用一些非零矩阵把它们都包起来,
    要求这些矩阵的面积和最小,求这个面积和
    题解:我们计算出以每两个点为矩形顶点所构成的矩形面积和包含的点子集,
    然后对这些子集进行状态DP,求全集的最小覆盖
*/
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
const int N=16;
int n,dp[1<<N],x[N],y[N];
struct Rect{
    int coverd,area;
    Rect(const int& coverd,const int& area):coverd(coverd),area(area){}
    void add(int i){coverd|=1<<i;} 
};
bool in(int i,int j,int k){return((x[i]-x[k])*(x[j]-x[k])<=0)&&((y[i]-y[k])*(y[j]-y[k])<=0);}
int main(){
    while(scanf("%d",&n)&&n){
        for(int i=0;i<n;i++)scanf("%d%d",&x[i],&y[i]);
        vector<Rect> VR;
        for(int i=0;i<n;i++){
            for(int j=i+1;j<n;j++){
                Rect r((1<<i)|(1<<j),max(1,abs(x[i]-x[j]))*max(1,abs(y[i]-y[j])));
                if(i!=j)for(int k=0;k<n;k++)if(in(i,j,k))r.add(k);  
                VR.push_back(r);
            }
        }memset(dp,0x3f,sizeof(dp));
        dp[0]=0; vector<Rect>::iterator it;
        for(it=VR.begin();it!=VR.end();it++){
            for(int s=0;s<(1<<n);s++){
                int ns=s|it->coverd;
                if(ns!=s)dp[ns]=min(dp[ns],dp[s]+it->area);
            }
        }printf("%d
",dp[(1<<n)-1]);
    }return 0;
}

POJ 1795:DNA Laboratory

/*
    题目大意:给出n个字符串,求一个最小长度的串,该串包含给出的所有字符串。
    要求长度最小且字典序最小。
    题解:dp[i][s]表示包括s集合字符串的第i个字符串为开头的最小值
    从后往前贪心得到最小值,然后从前往后搜索得出最小字典序的答案
*/
#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
using namespace std;
const int N=17,INF=0x3f3f3f3f;
string str[N],ans;
int T,Cas=0,n,dp[N][1<<N],cost[N][N];
void init(){
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++){
            if(i!=j&&str[i].find(str[j])!=string::npos)str[j]=str[i];
        }
    }sort(str,str+n);
    n=unique(str,str+n)-str;
    memset(cost,0,sizeof(cost));
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++)if(i!=j){
            int len=min(str[i].length(),str[j].length());
            for(int k=0;k<=len;k++){
                if(str[i].substr(str[i].length()-k)==str[j].substr(0,k)){
                    cost[i][j]=str[i].length()-k;
                }
            }
        }
    }
}
void dfs(int id,int s){
    if(s==0)return;
    string tmp; int nxt=-1;
    for(int i=0;i<n;i++)if(i!=id&&(s>>i&1)){
        if(dp[id][s]==dp[i][s&~(1<<id)]+cost[id][i]){
            string t=str[i].substr(str[id].length()-cost[id][i],str[i].length());
            if(nxt==-1||t<tmp){tmp=t;nxt=i;}
        }
    }ans+=tmp;
    dfs(nxt,s&~(1<<id));
}
int main(){
    cin>>T;
    while(T--){
        cin>>n;
        for(int i=0;i<n;i++)cin>>str[i];
        if(n>1){
            init();
            for(int i=0;i<=n;i++)fill(dp[i],dp[i]+(1<<n),INF);
            for(int i=0;i<n;i++)dp[i][1<<i]=str[i].length();
            for(int s=0;s<1<<n;s++){
                for(int j=0;j<n;j++)if(dp[j][s]!=INF&&(s>>j&1)){
                    for(int i=0;i<n;i++)if(!(s>>i&1)){
                        dp[i][s|1<<i]=min(dp[i][s|1<<i],dp[j][s]+cost[i][j]);
                    }
                }
            }int id=0;
            for(int i=1;i<n;i++){
                if(dp[i][(1<<n)-1]<dp[id][(1<<n)-1])id=i;
            }ans=str[id];
            dfs(id,(1<<n)-1);
        }else ans=str[0];
        cout<<"Scenario #"<<++Cas<<":"<< endl;  
        cout<<ans<<endl<<endl; 
    }return 0;
}

POJ 3411:Paid Roads

/*
    题目大意:从a到b的路,如果已经访问过c那么路费为p否则为r,问从1到n的最短路
    题解:搜索记录每个点在该回溯中被访问的次数,
    因为这张图最多只有十个点,所以如果一个点被访问的次数超过3,
    那么一定是重复走环路了,可证明重复走环路答案一定不是最优的因此可以停止继续搜索这个点。
*/
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int INF=0x3f3f3f3f;
int n,m,vis[20],ans;
struct data{int a,b,c,p,r;}u[20];
void dfs(int a,int w){
    if(a==n&&ans>w){ans=w;return;}
    for(int i=1;i<=m;i++){
        if(a==u[i].a&&vis[u[i].b]<=3){
            int b=u[i].b;
            vis[b]++;
            if(vis[u[i].c])dfs(b,w+u[i].p);
            else dfs(b,w+u[i].r);
            vis[b]--;
        }   
    }return;
}
int main(){
    while(~scanf("%d%d",&n,&m)){
        memset(vis,0,sizeof(vis)); vis[1]=1;
        ans=INF;
        for(int i=1;i<=m;i++)scanf("%d%d%d%d%d",&u[i].a,&u[i].b,&u[i].c,&u[i].p,&u[i].r);
        dfs(1,0);
        if(ans==INF)puts("impossible");
        else printf("%d
",ans);
    }return 0;
}

POJ 3420:Quad Tiling

/*
    题目大意:给出一个4*n的矩阵,求用1*2的骨牌填满有多少方案数
    题解:弄出不同情况的继承关系,用矩阵递推即可。
*/
#include <cstdio>
#include <vector>
#define rep(i,n) for(int i=0;i<n;i++)
using namespace std;
typedef vector<vector<int> > mat;
typedef long long LL; 
int n,mod;
mat mul(mat &A,mat &B){
    mat C(A.size(),vector<int>(B[0].size()));
    rep(i,A.size())rep(j,B[0].size())rep(k,B.size())C[i][j]=(C[i][j]+A[i][k]*B[k][j])%mod;
    return C;
}
mat pow(mat A,LL n){
    mat B(A.size(),vector<int>(A.size()));
    rep(i,A.size())B[i][i]=1;
    for(;n;n>>=1){if(n&1)B=mul(B,A);A=mul(A,A);}
    return B;
}
int main(){
    while(scanf("%d%d",&n,&mod),n+mod){  
        mat b(6,vector<int>(6)); 
        b[0][0]=1; b[0][1]=1; b[0][2]=1; b[0][3]=1; b[0][4]=1; b[0][5]=0;  
        b[1][0]=1; b[1][1]=0; b[1][2]=1; b[1][3]=0; b[1][4]=0; b[1][5]=0;  
        b[2][0]=1; b[2][1]=1; b[2][2]=0; b[2][3]=0; b[2][4]=0; b[2][5]=0;  
        b[3][0]=1; b[3][1]=0; b[3][2]=0; b[3][3]=0; b[3][4]=0; b[3][5]=1;  
        b[4][0]=1; b[4][1]=0; b[4][2]=0; b[4][3]=0; b[4][4]=0; b[4][5]=0;  
        b[5][0]=0; b[5][1]=0; b[5][2]=0; b[5][3]=1; b[5][4]=0; b[5][5]=0;  
        printf("%d
",pow(b,n)[0][0]);  
    }return 0;  
}

POJ 3735:Training little cats

/*
    题目大意:有一排小猫,给出一系列操作,包括给一只猫一颗花生,
    让某只猫吃完所有的花生以及交换两只猫的花生,
    求完成m次操作集合之后每只猫的花生数量
    题解:创建一个1*(n+1)的初始矩阵,
    对于给第i只猫一个花生就相当于乘上修改了A[0][i]的单位矩阵
    如给第一只猫一个花生就修改A[0][1]为1 
    得到:
        1 1 0 0
        0 1 0 0
        0 0 1 0
        0 0 0 1
    然后我们用初始矩阵去乘这个矩阵,就完成了加的操作
    对于让第i只猫吃完所有的花生,我们将A[i][i]修改为0
    如让第三只猫吃完花生,那么就相当于将A[3][3]改为0
    得到
        1 0 0 0
        0 1 0 0
        0 0 1 0
        0 0 0 0
    然后我们用初始矩阵去乘这个矩阵,就完成了清零的操作
    对于交换花生的操作,我们交换单位矩阵的第x和y行,然后相乘即可
    比如交换第一只猫和第三只猫的花生,我们就得到
        1 0 0 0
        0 0 0 1
        0 0 1 0
        0 1 0 0
    乘到初始矩阵上即可
    我们发现经过这些操作,我们最后会得到一个操作集合的矩阵表达
    而这些操作的变换其实不用整个乘上矩阵,只要直接修改单位矩阵的部分值即可
    第一种操作的影响可以转化为A[0][x]++
    第二种操作的影响可以转化为A[0~n][x]=0
    第三种操作的影响可以转化为i_0^n swap(A[i][x],A[i][y])
    那么我们直接计算转置矩阵即可。
*/
#include <cstdio>
#include <vector>
#define rep(i,n) for(int i=0;i<n;i++)
using namespace std;
typedef long long LL; 
typedef vector<vector<LL> > mat;
LL n,m,k,x,y;
char op[10];
mat mul(mat &A,mat &B){
    mat C(A.size(),vector<LL>(B[0].size()));
    rep(i,A.size())rep(k,B.size())if(A[i][k]){
        rep(j,B[0].size())C[i][j]+=A[i][k]*B[k][j];
    }return C;
}
mat pow(mat A,LL n){
    mat B(A.size(),vector<LL>(A.size()));
    rep(i,A.size())B[i][i]=1;
    for(;n;n>>=1){if(n&1)B=mul(B,A);A=mul(A,A);}
    return B;
}
int main(){
    while(scanf("%lld%lld%lld",&n,&m,&k),n+m+k){  
        mat A(n+1,vector<LL>(n+1));
        rep(i,n+1)A[i][i]=1;
        while(k--){
            scanf("%s",&op);
            if(op[0]=='g'){
                scanf("%lld",&x);
                A[0][x]++;
            }else if(op[0]=='e'){
                scanf("%lld",&x);
                rep(i,n+1)A[i][x]=0;
            }else{
                scanf("%lld%lld",&x,&y);
                rep(i,n+1)swap(A[i][x],A[i][y]);
            }
        }if(m){
            A=pow(A,m);
            printf("%lld",A[0][1]);
            for(int i=2;i<=n;i++)printf(" %lld",A[0][i]);
            puts("");
        }else{
            printf("0");
            rep(i,n-1)printf(" 0");
            puts("");
        }
    }return 0;
}

POJ 3171:Cleaning Shifts

/*
    题目大意:给出一些区间和他们的价值,求覆盖一整条线段的最小代价
    题解:我们发现对区间右端点排序后有dp[r]=min(dp[l-1~r-1])+s
    而对于求最小值我们可以用线段树优化
*/
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <climits>
using namespace std;
const int N=100010;
int T[N*4],n,m,E,M,x,y;
struct data{int l,r,s;}p[N];
bool cmp(data a,data b){return a.r<b.r;}
int main(){
    while(~scanf("%d%d%d",&n,&m,&E)){
        E=E-m+2;
        for(M=1;M<E;M<<=1);
        fill(T,T+M+E+1,INT_MAX/2);
        T[M+1]=0;
        for(int i=0;i<n;i++){
            scanf("%d%d%d",&p[i].l,&p[i].r,&p[i].s);
            p[i].l=p[i].l-m+2;
            p[i].r=p[i].r-m+2;
        }sort(p,p+n,cmp);
        for(int i=0;i<n;i++){
            int t=INT_MAX/2;
            x=p[i].l+M-2; y=p[i].r+M;
            while(x^y^1>0){
                if(~x&1)t=min(t,T[x+1]);
                if(y&1)t=min(t,T[y-1]);
                x>>=1;y>>=1;
            }T[M+p[i].r]=min(T[M+p[i].r],t+p[i].s);
            for(x=(M+p[i].r)/2;x;x/=2)T[x]=min(T[x<<1],T[(x<<1)^1]);
        }printf("%d
",T[E+M]==INT_MAX/2?-1:T[E+M]);
    }return 0;
}