【洛谷】P4728 [HNOI2009]双递增序列(动态规划)
题意
给出一个长度为偶数的序列,判断是否能将该序列分成两个长度为 \(\dfrac{n}{2}\) 的严格上升子序列。
数据范围
\(1 \leq n \leq 2000\),\(1 \leq m \leq 25\),\(0 \le a_i \le 10^6\) 。
思路
首先发现本题有两个限制条件,一个条件是分成两个长度为 \(\dfrac{n}{2}\) 的子序列。另一个条件是严格单调递增。
对于第二个条件,可以想到一个贪心性质:相同长度子序列中,最后一个数字最小的最优。因为这样更容易在序列后面再扩充一个数(在导弹拦截中的 \(O(n \log n)\) 做法就利用了这个贪心性质)。
同时可以发现,如果在前 \(i\) 个数中选了 \(j\) 个数到第一个序列中,那么剩下的 \(i-j\) 个数就必然在另一个序列中。同时这两个序列都必须单调递增。由于只需要保证新选的数比当前序列的最后一个数大即可。故可以考虑动态规划:
设 \(f[i][j]\) 表示在前 \(i\) 个数中选了 \(j\) 个数且 \(a_i\) 在第一个序列中时的所有合法方案中,另一个序列的最后一个数的最小值(根据上面的贪心性质)。
初始时,将 \(f\) 全部赋值为无穷大,再将 \(f[1][1]\) 设成一个负数(因为此时另一个序列不存在,同时 \(a_i \geq 0\))。
根据状态设计,从前 \(i\) 个数转移到前 \(i+1\) 个数需要分 \(a_{i+1}\) 是否分到 \(a_i\) 所在的序列进行讨论:
1.\(a_i\) 和 \(a_{i+1}\) 在同一个序列中。当然,前提条件是 \(a_i < a_{i+1}\),此时另一个序列没有变化,得到转移方程:
\(f[i+1][j+1]=\min(f[i+1][j+1],f[i][j])\)
2.\(a_i\) 和 \(a_{i+1}\) 不在同一个序列中。那么也就是把 \(a_{i+1}\) 放在最后一个数为 \(f[i][j]\) 的序列中。也就能得到转移的前提条件是 \(a_{i+1} > f[i][j]\),此时 \(a_i\) 所在的长度为 \(j\) 的序列没有发生变化,并且 \(a_i\) 成了另一个序列的最后一个数,所以 \(a_{i+1}\) 所在的序列长度就是 \(i+1-j\)。可以得到转移方程:
\(f[i+1][i+1-j]=\min(f[i+1][i+1-j],a[i])\)。
由于最终的两个序列长度都是 \(n/2\),故最后只需要判断一下 \(f[n][n/2]\) 是否为无穷大即可。
code:
#include<cstdio>
#include<cstring>
using namespace std;
const int N=2020;
const int INF=0x3f3f3f3f;
int n,a[N],f[N][N];
int min(int a,int b){return a<b?a:b;}
int main()
{
// freopen("nlc.in","r",stdin);
int T;scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
memset(f,0x3f,sizeof(f));
f[1][1]=-1;
for(int i=1;i<n;i++)
for(int j=1;j<=min(n/2,i);j++)
{
if(a[i+1]>f[i][j]) f[i+1][i+1-j]=min(f[i+1][i+1-j],a[i]);
if(a[i+1]>a[i]) f[i+1][j+1]=min(f[i+1][j+1],f[i][j]);
}
// printf("%d\n",f[n][n/2]);
if(f[n][n/2]==INF) puts("No!");
else puts("Yes!");
}
return 0;
}

浙公网安备 33010602011771号