【专题集训】简单动态规划
T1 (2004 提高) 合唱队形
题目
\(n\) 位同学站成一排,音乐老师要请其中的 \(n-k\) 位同学出列,使得剩下的 \(k\) 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 \(k\) 位同学从左到右依次编号为 \(1,2,\) … \(,k\),他们的身高分别为 \(t_1,t_2,\) … \(,t_k\),则他们的身高满足 \(t_1< \cdots <t_i>t_{i+1}>\) … \(>t_k(1\le i\le k)\)。
你的任务是,已知所有 \(n\) 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
思路
观察题面发现合唱队形是一个最长上升子序列和最长下降子序列拼成的序列,若 \(m\) 为上升序列和下降序列的分割点,则 \(1 \sim m\) 为上升序列,\(m+1 \sim n\) 为下降序列。设 \(f_{up}(i)\) 表示以 \(1 \sim i\) 的最长上升子序列长度,\(f_{down}(i)\) 为以 \(i \sim n\) 的最长下降子序列长度,分别做一个 \(O(n^2)\) 的 DP,最后枚举一下 \(m\) 的位置,去一个最大值即可。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=107;
int n,a[N],up[N],down[N];
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) up[i]=down[i]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
if(a[j]<a[i]) up[i]=max(up[i],up[j]+1);
for(int i=n;i>=2;i--)
for(int j=n;j>i;j--)
if(a[i]>a[j]) down[i]=max(down[i],down[j]+1);
int ans=-1;
for(int mid=1;mid<=n;mid++)
ans=max(ans,up[mid]+down[mid]-1);
cout<<n-ans;
return 0;
}
T5 (2005 提高) 过河
题目
在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有 \(m\) 个石子,坐标为 \(a_1,\cdots,a_m\)。可以把独木桥上青蛙可能到达的点看成数轴上的一串整点,坐标为 \(0\) 的点表示桥的起点,坐标为 \(l\) 的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离(青蛙的跳跃能力)是 \(s\) 到 \(t\) 之间的任意正整数(包括 \(s,t\))。当青蛙跳到或跳过坐标为 \(l\) 的点时,就算青蛙已经跳出了独木桥,求青蛙要想过河,最少需要踩到的石子数。
思路
看起来像是一个简单 DP 题,设 \(dp_i\) 为青蛙到 \(i\) 时踩到石头的最小值,状态转移方程:
其中 \(u_i\) 表示 \(i\) 处有没有石子。此时只需要双层 for 循环,外层枚举青蛙的位置 \(i\),内层枚举青蛙本次跳跃的位移 \(j\) 即可,时间复杂度为 \(O(l \times t)\)。
结果代码超时并且超空间,原因是我们必须枚举青蛙的距离 \(i(0 \le i \le l)\),并且还要开一个长度为 \(l\) 的 DP 数组,只能拿到 \(1 \le l \le 10^4\) 的部分分。如何优化?
仔细观察发现题面中石头的个数 \(m \le 100\),所以可能会出现一个十字后面隔了一百万格之后才会出现下一个石子,中间一次最多跳 \(t\) 格,这出现了很多不必要的决策,浪费了很多不必要的空间,因此,考虑将相邻两个石子之间的距离缩短,即将石子“拉近”。
定义“距离”为与上一个石头的距离。对于两个距离非常大的相邻的石头 \(i\) 和 \(i-1\),它们的距离 \(d=a_{i}-a_{i-1}\),取 \(k=t^2\),如果距离 \(d \ge 2k\) 则将距离缩短 \(d - (d \bmod k + k)\),记录缩短距离的总和 \(S\)。在每个石头开始缩短前,要更新一下石头的距离(因为前面的石头的坐标变小了,所以当前石头的坐标也要变小)。
- Q:为什么 \(k=t^2\)?
- A:如果 \(k\) 取太小(比如 \(k=2\)),青蛙就可以一下子跳过去,不符合原数据。如果 \(k\) 取太大,会导致所有石头的距离太长,导致超时或超内存。一个不能被 \([s,t]\) 之间的数的和表示的最大的数是 \(s,t\) 的最小公倍数,超过 \(s,t\) 的最小公倍数的数都可以用 \(s,t\) 以 \(as+b(s+1)+ \cdots + ct\) 的形式表示。具体可参考 P3951 [NOIP 2017 提高组] 小凯的疑惑 这一题。
- Q:为什么距离 \(d \ge 2k\) 才能缩短?
- A:考虑两个石头 \(A,B\),距离分别为 \(d_A=1218k+7,d_B=10\),在不考虑距离 \(d \ge 2k\) 的情况下,石头 A 的距离会被缩成 \(7\),比 B 的距离还小,这样青蛙就能一下子跳过去了而非先走一段很长的距离再跳。
- Q:为什么缩短的距离是 \(d - (d \bmod k + k)\)?
- A:因为我们要把距离 \(d\) 缩短若干次,每次缩短 \(k\),最后的距离为 \(d \bmod k + k\)。
最后就是动态规划,状态转移方程和部分分代码一样:
注意,青蛙跳出河也算一种合法的答案,因为题面说“当青蛙跳到或跳过坐标为 \(l\) 的点时,就算青蛙已经跳出了独木桥。”,故要将 \(dp_{a_m} \sim dp_{a_m}+t\) 的所有答案取个最小值再输出。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int L=1e9+7;
constexpr int T=12;
constexpr int M=107;
constexpr int N=20097;
int l,s,t,m;
int dp[N]; //dp[x]表示位移为x的时候踩到石头的最小值
int a[M];
bool stone[N]; //此处是否有石头
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
cin>>l>>s>>t>>m;
const int k=t*t; //最近的缩进距离
int S=0; //累计拉进的距离
for(int i=1;i<=m;i++) cin>>a[i];
sort(a+1,a+m+1); //注意:石子可能不按顺序给s
for(int i=1;i<=m;i++) //拉进石子
{
a[i]=a[i]-S; //更新石子距离(前面拉进的距离在这里更新)
int d=a[i]-a[i-1]; //相邻两个石子间的距离
if(d>=2*k) //可以拉进
{
int deltax=d-(d%k+k); //欲拉进的距离
a[i]-=deltax; //石头拉进
S+=deltax; //更新累计拉进的距离
}
}
for(int i=1;i<=m;i++) stone[a[i]]=1; //标记石头位置
for(int i=1;i<a[m]+t;i++) dp[i]=1218; //跳到l,l+1,l+2,...,l+t-1位置都是合法的,因为可以跳出桥外
dp[0]=stone[0]; //最开始有没有踩到石头取决于第一个站的位置有没有石头
for(int i=0;i<=a[m];i++) //外层循环枚举青蛙站在的地方
for(int j=s;j<=t;j++) //内层循环枚举青蛙的位移,在[s,t]之间
dp[i+j]=min(dp[i+j],dp[i]+stone[i+j]);
//状态转移方程:若青蛙的位置在i+j处,则可以从i,i+1,...,i+j-1处跳过来
int ans=1218;
for(int i=a[m];i<a[m]+t;i++) ans=min(ans,dp[i]); //青蛙跳到桥外面的情况也要考虑
cout<<ans<<endl;
return 0;
}
/*
将两块石头的距离压缩至>=10*t
注意:跳到l,l+1,l+2,...,l+t-1位置都是合法的,因为可以跳出桥外
*/
T9 (2010 提高) 乌龟棋
题目
乌龟棋的棋盘是一行 \(N\) 个格子,每个格子上一个分数 \(a_i\)(非负整数)。棋盘第 \(1\) 格是唯一的起点,第 \(N\) 格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。
乌龟棋中 \(M\) 张爬行卡片,分成 \(4\) 种不同的类型(\(M\) 张卡片中不一定包含所有 \(4\) 种类型的卡片,见样例),每种类型的卡片上分别标有 \(1,2,3,4\) 四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。
游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的分数总和。你需要找到一种卡片使用顺序使得最终游戏得分最多。
思路
下面用“\(x\) 号卡片”代替“卡片上的数字为 \(x\) 的卡片”,\(c_x\) 为 \(x\) 号卡片的个数。
题目说到“每种卡片的张数不会超过 \(40\)”,可以考虑四维 DP:设 \(f(i,j,k,l)\) 为使用了 \(i\) 张 \(1\) 号卡片,\(j\) 张 \(2\) 号卡片,\(k\) 张 \(3\) 号卡片,\(l\) 张 \(4\) 号卡片。在使用 \(i,j,k,l\) 张 \(1,2,3,4\) 号卡片后,小乌龟对地位移 \(\Delta x=i+2j+3k+4l\),小乌龟初始时在位置 \(x_0=1\),所以小乌龟的位置将会变成 \(x=1+i+2j+3k+4l\),并吃到 \(x\) 处的数字 \(a_x\)。
如果某号卡片没有用完,就可以用这一张这种卡片。所以 \(f(i,j,k,l)\) 可以由 \(f(i-1,j,k,l),f(i,j-1,k,l),f(i,j,k-1,l),f(i,j,k,l-1)\) 转移过来。初始时乌龟的位置 \(x=0\),所以在一张卡片都不用的情况下得分为 \(a_1\),即 \(f(0,0,0,0)=a_x\)。对于 \(0 \le i \le c_1,0 \le j \le c_2,0 \le k \le c_3,0 \le l \le c_4\),均有如下的状态转移:
最终 \(f(c_1,c_2,c_3,c_4)\) 为答案。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int M=45;
constexpr int N=355;
int n,m;
int dp[M][M][M][M];
int a[N],b;
int c[7];
int main()
{
// freopen("neuvillette.in","r",stdin);
// freopen("neuvillette.out","w",stdout);
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);
c[b]++;
}
dp[0][0][0][0]=a[1];
for(int i=0;i<=c[1];i++)
for(int j=0;j<=c[2];j++)
for(int k=0;k<=c[3];k++)
for(int l=0;l<=c[4];l++)
{
int pos=1+i*1+j*2+k*3+l*4;
if(i) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i-1][j][k][l]+a[pos]);
if(j) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j-1][k][l]+a[pos]);
if(k) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k-1][l]+a[pos]);
if(l) dp[i][j][k][l]=max(dp[i][j][k][l],dp[i][j][k][l-1]+a[pos]);
}
printf("%d",dp[c[1]][c[2]][c[3]][c[4]]);
return 0;
}

浙公网安备 33010602011771号