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);
}