• T2980 LR棋盘【Dp+空间/时间优化】


    Online Judge:未知

    Label:Dp+滚动+前缀和优化

    题目描述

    有一个长度为1*n的棋盘,有一些棋子在上面,标记为L和R。

    每次操作可以把标记为L的棋子,向左移动一格,把标记为R的棋子,向右移动一格,前提条件是目标格子为空。结果任意多次操作之后,棋盘会有多少种不同的状态?

    输入

    第一行一个字符串,包含'L','R','.'。

    输出

    输出所有不同的棋盘状态,对1e9+7 求余。

    样例

    Input

    R..
    
    R.L
    
    ...R..L..L
    
    R..L......LLR.RRL..RR.....RR.L.RR.....L..R....RR.........R..RLL..RL...RLL.LLRR......RLRRR...R......R
    

    Output

    3
        
    3
        
    22
    
    709788833
    

    Hint

    样例2的最终状态为 "R.L",".RL","RL."
    对于10%的数据,棋子数为1
    对于30%的数据,棋子数不超过2,棋盘长度不超过10
    对于40%的数据,棋子数不超过5,棋盘长度不超过10
    对于60%的数据,棋子数不超过50,棋盘长度不超过500
    对于90%的数据,棋子数不超过100,棋盘长度不超过10000
    对于100%的数据,棋子数不超过2000,棋盘长度不超过20000。


    题解

    40pts

    观察到一个性质,棋子与棋子之间的相对位置是不会变的。考虑枚举每个棋子的位置,最后检验一遍。运行次数大约为(C(10,5)*5)

    大致代码如下

    namespace p40{
    	int a[12],ans=0;
    	inline bool check(){
    		for(int i=1;i<=cnt;++i){
    			if(s[pos[i]]=='L'&&a[i]>pos[i])return 0;
    			if(s[pos[i]]=='R'&&a[i]<pos[i])return 0;
    		}
    		return 1;
    	}
    	void dfs(int x,int lst){
    		if(x==cnt+1){if(check())ans++;return;}
    		for(int i=lst+1;i<=n;++i){a[x]=i;dfs(x+1,i);}
    	}
    	void solve(){dfs(1,0);printf("%d
    ",ans);}
    }
    int main(){
        scanf("%s",s+1);
    	n=strlen(s+1);
    	for(int i=1;i<=n;++i){
    		if(s[i]=='.')continue;
    		pos[++cnt]=i;
    	}	
    }
    

    100pts

    L只能往左边走,R只能往右边走。由此我们可以循环一下计算出每个L,R能到达的最左最右位置((li[i],ri[i]) (i∈[1,cnt]))。

    代码如下, 应该可以写得更简洁一点。

    void pre(){
    	int now=n+1,i,j;
    	for(int i=cnt;i>=1;i--)if(s[pos[i]]!='.'){
    		ri[i]=now-1;
    		if(s[pos[i]]=='L')now=pos[i];
    	}
    	now=0;
    	for(int i=1;i<=cnt;++i)if(s[pos[i]]!='.'){
    		li[i]=now+1;
    		if(s[pos[i]]=='R')now=pos[i];
    	}
    	for(int i=1;i<=cnt;++i){
    		if(s[pos[i]]=='L')ri[i]=pos[i];
    		if(s[pos[i]]=='R')li[i]=pos[i];
    	}
    }
    

    这样问题变成了:

    给定cnt个空,第i个空可以填的数字范围为(li[i])~(ri[i]),问有多少种方案,使得序列严格递增。

    之前类似的问题有遇到过:T2988 删除数字【状压Dp+前缀和优化】

    下面重新分析一下这类问题。

    假如题目令填完的序列单调递增(严格),元素个数为(cnt),元素取值范围为(n)

    定义状态(dp[i][j])表示,已经填完了前(i)个,且第(i)个空填的数字大小为(j)时,前面的方案数。(定义成后面,逆推也可以)

    • 最朴素的转移就是枚举i,枚举i取的数字,枚举i-1取的数字,时间复杂度为(O(cntcdot n^2)),空间复杂度为(O(cntcdot n))

    • 时间优化:利用前缀和/后缀和优化,免去枚举i-1取的数这一维,时间复杂度为(O(cntcdot n)),空间复杂度仍然为(O(cntcdot n))

    • 空间优化:比如说对于这道题,如果数组开成(dp[cnt][n]),第10个点会MLE。既然只用到前一个位置的状态,容易想到滚动掉第一维变成(dp[2][n])(tip1),也可以改变转移顺序,这样只用到一维(dp[cnt]),空间上大大优化(tip2)。

    综上时间复杂度最优为(O(cntcdot n)),dp数组大小只用开到(cnt)

    本题代码如下,预处理函数(pre)已经在前面给出:

    代码1:

    #include<bits/stdc++.h>
    #define mod 1000000007
    const int N=20005,maxn=2005;
    char s[N];
    int n,li[maxn],ri[maxn],cnt,pos[maxn],dp[maxn];
    void pre(){}
    int main(){
    	scanf("%s",s+1);
    	n=strlen(s+1);
    	for(int i=1;i<=n;++i)if(s[i]!='.')pos[++cnt]=i;
    	pre();
    	dp[0]=1;
    	for(int i=1;i<=n;i++)for(int j=cnt;j>=1;j--){
    		if(li[j]<=i&&i<=ri[j])dp[j]=(dp[j]+dp[j-1])%mod;
    	}
    	printf("%d",dp[cnt]);
    }
    

    奉上赛时代码,用了滚动优化空间,而且是逆推,看起来比较乱

    代码2

    #include<bits/stdc++.h>
    #define mod 1000000007
    using namespace std;
    typedef long long ll;
    bool nc1;
    const int maxn=20005;
    char s[maxn];
    int n,cnt,pos[2010];
    ll dp[2][maxn],sum[maxn];
    namespace p40{
        int a[12],ans=0;
        inline bool check(){
            for(int i=1;i<=cnt;++i){
                if(s[pos[i]]=='L'&&a[i]>pos[i])return 0;
                if(s[pos[i]]=='R'&&a[i]<pos[i])return 0;
            }
            return 1;
        }
        void dfs(int x,int lst){
            if(x==cnt+1){if(check())ans++;return;}
            for(int i=lst+1;i<=n;++i){a[x]=i;dfs(x+1,i);}
        }
        void solve(){dfs(1,0);printf("%d
    ",ans);}
    }
    inline void Do(ll &x,ll y){
        x+=y;
        if(x>=mod)x-=mod;
    }
    int li[2010],ri[2010];
    bool nc2;
    int main(){
    //  cout<<(&nc2-&nc1)/1024/1024<<endl;
    //  freopen("board.in","r",stdin);
    //  freopen("board.out","w",stdout);
        scanf("%s",s+1);
        n=strlen(s+1);
        for(int i=1;i<=n;++i){
            if(s[i]=='.')continue;
            pos[++cnt]=i;
        }   
     
        if(n<=10){
            p40::solve();
            return 0;
        }
        register int now=n+1,i,j;
        for(i=cnt;i>=1;i--)if(s[pos[i]]!='.'){
            ri[i]=now-1;
            if(s[pos[i]]=='L')now=pos[i];
        }
        now=0;
        for(i=1;i<=cnt;++i)if(s[pos[i]]!='.'){
            li[i]=now+1;
            if(s[pos[i]]=='R')now=pos[i];
        }
        for(i=1;i<=cnt;++i){
            if(s[pos[i]]=='L')ri[i]=pos[i];
            if(s[pos[i]]=='R')li[i]=pos[i];
        }
         
        memset(dp,0,sizeof(dp));
        int g=0;
        for(i=li[cnt];i<=ri[cnt];++i)dp[g][i]=1;
        for(i=ri[cnt];i>=1;i--)sum[i]=(sum[i+1]+dp[g][i])%mod;
         
        for(i=cnt-1;i>=1;i--){
            g^=1;memset(dp[g],0,sizeof(dp[g])); 
            for(j=li[i];j<=ri[i];++j)Do(dp[g][j],sum[j+1]);
            memset(sum,0,sizeof(sum));
            for(j=ri[i];j>=1;j--){
                sum[j]=sum[j+1];
                Do(sum[j],dp[g][j]);
            }
        }
         
        printf("%lld
    ",(sum[li[1]]+mod)%mod);
    }
    
  • 相关阅读:
    oracle数据库的隔离级别MS
    常用的oracle数据备份恢复方法有哪些?每一种的应用场景是什么?如果我的数据库是1T,说说你的备份 规划方案MS
    oracle中不能把字段类型改为clob,blob
    oracle dataguard中的常用视图(DG中常用视图)
    导出oracle awr 报告的操作
    mysql 部分命令
    企业微信HOOK逆向(sdk)——登录二维码api
    学习 游泳
    Celery Bo7
    算法时间复杂度
  • 原文地址:https://www.cnblogs.com/Tieechal/p/11603597.html
Copyright © 2020-2023  润新知