题目大意
定义波浪序列为满足 a1<a2>a3<a4... 的序列,现在给出两个数组a(长为n)和b(长为m),从a中选出满足波浪序列一个子序列f,b中选出满足波浪序列的子序列g,求有多少中选法满足f=g。
数组长度 n,m≤2000 , a[i]、b[i]≤2000
分析
题目很像最长公共子序列,从动态规划的角度去思考.
定义:
dp[i][j][t] 表示a数组以i结尾,b数组以j结尾的方案数(t为0表示结尾是波谷,1表示波峰)
最暴力的转移方程:
dp[i][j][t]=⎧⎩⎨⎪⎪0∑i−1x=1∑j−1y=1dp[x][y][(t+1)%2],(a[x]=b[y])(a[x]<a[i]时t=0,a[x]>a[i]时t=1)a[i]≠b[j]a[i]=a[j]
这样复杂度是 O(n4) ,根据数据这道题复杂度不会超过 O(n3) ,所以应该想 O(n2) 或 O(n2logn) 的复杂度的算法.
由于状态的个数就是 n2 了,我们尝试对状态的转移进行优化。
一个优化的思想就是信息的重用,在当前的状态转移过程中用到的信息保留下来以便在之后的状态转移中使用。
对于 a[i]=a[j] 的情况,在求 dp[i][j][t] 的时候我们希望知道的是i前面和j前面有多少可行方案数,正如前面所说,我们应该对状态的转移进行优化,优化状态的转移也就是维护“方案数”这个信息。
在状态转移的两层循环中,外层的是i,内层的是j,也就是说在j变化的过程中i是不变的.
我们用一个数组 sum[i][j][t] 表示对于a数组中的前i个数,b数组中以j为结尾的方案数的个数(t为0表示末尾是波谷,1表示波峰)。sum[i][j][t] 表示 ∑{dp[k][j][t]|1≤k<j}
有了这个信息之后我们就可以在对b数组进行扫描(j从1到m)的时候将满足 a[i]=a[j] 的 sum[i][j][t] 相加即可得到 dp[i][j][(t+1)%2]
总结
这道题在dp的过程中通过一个sum数组来优化状态的转移,对sum数组的求和得到新的状态的值,巧妙的是sum数组的更新也是通过对dp数组的一个求和。
由于状态的转移是通过sum数组得到dp[i]不需要用到dp[i-k],所以在实现的过程中可以将i的这一维数组降掉(空间上的降不是时间上)。相当于背包问题的一维实现。代码
#include<cstdio>
#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<queue>
#include<map>
#include<algorithm>
#include<set>
#include<stack>
using namespace std;
const long long int MOD=998244353;
const int MAXN=2005;
int T;
int n,m;
int a[MAXN];
int b[MAXN];
int dp[MAXN][2];
int sum[MAXN][2];
void Work()
{
memset(dp,0,sizeof(dp));
memset(sum,0,sizeof(sum));
long long int ans=0;
long long int cnt0,cnt1;//cnt表示波谷的个数,cnt1表示波峰的个数
for(int i=1;i<=n;i++)
{
cnt0=0;cnt1=1;
for(int j=1;j<=m;j++)
{
dp[j][0]=dp[j][1]=0;
if(a[i]==b[j])
{
dp[j][0]=cnt1;
dp[j][1]=cnt0;
ans=(ans+cnt0+cnt1)%MOD;
}
else if(a[i]>b[j]) cnt0=(cnt0+sum[j][0])%MOD;
else cnt1=(cnt1+sum[j][1])%MOD;
}
for(int j=1;j<=m;j++)
{
sum[j][0]=(sum[j][0]+dp[j][0])%MOD;
sum[j][1]=(sum[j][1]+dp[j][1])%MOD;
}
}
cout<<ans<<endl;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=m;i++)scanf("%d",&b[i]);
Work();
}
return 0;
}
/*
1
3 5
1 5 3
4 1 1 5 3
*/