【算法刷题】动态规划 Day2
今天刷了三道DP
直接看题目:
P1541 [NOIP 2010 提高组] 乌龟棋
题目背景
NOIP2010 提高组 T2
题目描述
小明过生日的时候,爸爸送给他一副乌龟棋当作礼物。
乌龟棋的棋盘是一行 \(N\) 个格子,每个格子上一个分数(非负整数)。棋盘第 \(1\) 格是唯一的起点,第 \(N\) 格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。
乌龟棋中 \(M\) 张爬行卡片,分成 \(4\) 种不同的类型(\(M\) 张卡片中不一定包含所有 \(4\) 种类型的卡片,见样例),每种类型的卡片上分别标有 \(1,2,3,4\) 四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。
游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的分数总和。
很明显,用不同的爬行卡片使用顺序会使得最终游戏的得分不同,小明想要找到一种卡片使用顺序使得最终游戏得分最多。
现在,告诉你棋盘上每个格子的分数和所有的爬行卡片,你能告诉小明,他最多能得到多少分吗?
输入格式
每行中两个数之间用一个空格隔开。
第 \(1\) 行 \(2\) 个正整数 \(N,M\),分别表示棋盘格子数和爬行卡片数。
第 \(2\) 行 \(N\) 个非负整数,\(a_1,a_2,…,a_N\),其中 \(a_i\) 表示棋盘第 \(i\) 个格子上的分数。
第 \(3\) 行 \(M\) 个整数,\(b_1,b_2,…,b_M\),表示 \(M\) 张爬行卡片上的数字。
输入数据保证到达终点时刚好用光 \(M\) 张爬行卡片。
输出格式
一个整数,表示小明最多能得到的分数。
输入输出样例 #1
输入 #1
9 5
6 10 14 2 8 8 18 5 17
1 3 1 2 1
输出 #1
73
说明/提示
每个测试点 1s。
小明使用爬行卡片顺序为 \(1,1,3,1,2\),得到的分数为 \(6+10+14+8+18+17=73\)。注意,由于起点是 \(1\),所以自动获得第 \(1\) 格的分数 \(6\)。
对于 \(30\%\) 的数据有 \(1≤N≤30,1≤M≤12\)。
对于 \(50\%\) 的数据有 \(1≤N≤120,1≤M≤50\),且 \(4\) 种爬行卡片,每种卡片的张数不会超过 \(20\)。
对于 \(100\%\) 的数据有 \(1≤N≤350,1≤M≤120\),且 \(4\) 种爬行卡片,每种卡片的张数不会超过 \(40\);\(0≤a_i≤100,1≤i≤N,1≤b_i≤4,1≤i≤M\)。
解法&&个人感想
今天看了一篇知乎,说其实DP题设数组的思路,在有些题上是比较板的
为什么?因为有的题的数据范围就暗示了你要怎么设,然后按着思路来就可以了
比如这题,我们就是将每个维数设成所用牌的数量,刚好不会爆
转移方程其实挺好推导的
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 405
#define maxm 45
using namespace std;
int n,m,x;
int ma[maxn];
int dp[maxm][maxm][maxm][maxm];
int cnt[5];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>ma[i];
}
for(int i=1;i<=m;i++){
cin>>x;
cnt[x]++;
}
dp[0][0][0][0]=ma[1];
for(int i=0;i<=cnt[1];i++){
for(int j=0;j<=cnt[2];j++){
for(int k=0;k<=cnt[3];k++){
for(int v=0;v<=cnt[4];v++){
if(i==0&&j==0&&v==0&&k==0) continue;
if(i>=1) dp[i][j][k][v]=max(dp[i][j][k][v],dp[i-1][j][k][v]+ma[1+i+2*j+3*k+4*v]);
if(j>=1) dp[i][j][k][v]=max(dp[i][j][k][v],dp[i][j-1][k][v]+ma[1+i+2*j+3*k+4*v]);
if(k>=1) dp[i][j][k][v]=max(dp[i][j][k][v],dp[i][j][k-1][v]+ma[1+i+2*j+3*k+4*v]);
if(v>=1) dp[i][j][k][v]=max(dp[i][j][k][v],dp[i][j][k][v-1]+ma[1+i+2*j+3*k+4*v]);
}
}
}
}
cout<<dp[cnt[1]][cnt[2]][cnt[3]][cnt[4]]<<endl;
system("pause");
return 0;
}
P1439 【模板】最长公共子序列
题目描述
给出 \(1,2,\ldots,n\) 的两个排列 \(P_1\) 和 \(P_2\) ,求它们的最长公共子序列。
输入格式
第一行是一个数 \(n\)。
接下来两行,每行为 \(n\) 个数,为自然数 \(1,2,\ldots,n\) 的一个排列。
输出格式
一个数,即最长公共子序列的长度。
输入输出样例 #1
输入 #1
5
3 2 1 4 5
1 2 3 4 5
输出 #1
3
说明/提示
- 对于 \(50\%\) 的数据, \(n \le 10^3\);
- 对于 \(100\%\) 的数据, \(n \le 10^5\)。
解法&&个人感想
这道题估计得絮絮叨叨讲很多,我们从最长上升子序列开始讲起吧
当然,对于它的\(n^2\)求法,我想大家应该都知道了
那么,我们现在来考虑它的\(nlogn\)求法
众所周知,当\(logn\)出现的时候,多半是伴随着二分或者带有二分思路(也可能是倍增)的算法出现
这里也不意外,我们把\(dp[i]\)改变定义,设置为最长上升子序列的长度为\(i\)时,其最后一个元素的值
那么,由于贪心思想,我们肯定要对每个\(i\)使得\(dp[i]\)尽可能小,方便后续更新
那么,我们只需要在遍历整个数组的时候,考虑把元素\(i\)插在\(dp\)数组的哪个位置,就可以了
这一步,因为\(dp\)数组必然有序,所以就是二分查找
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 10005
using namespace std;
int ma[maxn];
int dp[maxn];
int n;
const int INF=2147483647;
int main(){
memset(dp,INF,sizeof(dp));
cin>>n;
for(int i=1;i<=n;i++){
cin>>ma[i];
}
dp[1]=ma[1];
int len=1;
for(int i=2;i<=n;i++){
if(dp[len]<ma[i]){
dp[++len]=ma[i];
continue;
}
int l=lower_bound(dp+1,dp+1+len,ma[i])-dp;
dp[l]=min(dp[l],ma[i]);
}
cout<<len<<endl;
system("pause");
return 0;
}
然后,我们再来聊聊最长公共子序列
你可能会问,这玩意有啥关系?
其实关系可大了
我们用\(ma[i]\)表示数组\(b[i]\)中与\(a[i]\)中相同的数在\(a[i]\)中的对应位置
那么,如果我们遍历\(b[i]\),在\(b[i]\)中查询\(ma[b[i]]\)的最长上升子序列
因为\(b[i]\)是顺序遍历,所以,如果\(ma[b[i]]\)也上升,说明取出的这两个子列在顺序排列上完全相同,也就是最长公共子序列
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 100005
using namespace std;
ll a[maxn],b[maxn],ma[maxn];//B中第i个元素在A中的位置
ll dp[maxn];
int n;
const ll INF=1e18;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
ma[a[i]]=i;
dp[i]=INF;
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
int len=1;
dp[1]=ma[b[1]];
for(int i=2;i<=n;i++){
if(ma[b[i]]>dp[len]){
dp[++len]=ma[b[i]];
}
int l=lower_bound(dp+1,dp+1+len,ma[b[i]])-dp;
dp[l]=min(dp[l],ma[b[i]]);
}
cout<<len<<endl;
system("pause");
return 0;
}
P1020 [NOIP 1999 提高组] 导弹拦截
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
一行,若干个整数,中间由空格隔开。
输出格式
两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入输出样例 #1
输入 #1
389 207 155 300 299 170 158 65
输出 #1
6
2
说明/提示
对于前 \(50\%\) 数据(NOIP 原题数据),满足导弹的个数不超过 \(10^4\) 个。该部分数据总分共 \(100\) 分。可使用\(\mathcal O(n^2)\) 做法通过。
对于后 \(50\%\) 的数据,满足导弹的个数不超过 \(10^5\) 个。该部分数据总分也为 \(100\) 分。请使用 \(\mathcal O(n\log n)\) 做法通过。
对于全部数据,满足导弹的高度为正整数,且不超过 \(5\times 10^4\)。
此外本题开启 spj,每点两问,按问给分。
NOIP1999 提高组 第一题
\(\text{upd 2022.8.24}\):新增加一组 Hack 数据。
解法&&个人感想
前面,我们刚讲完最长上升子序列,这里第一问要求我们求最长不升子序列
其实,可以反向求最长不降子序列,更新的方法,只需要想清楚\(upper\_bound\)和\(lower\_bound\)的区别,然后直接转移,还是不难的
第二问的考点是\(DilWorth\)定理,感觉这个考点其实不常见,我贴一下定义:
偏序关系
对于二元关系
\(R \subseteq S \times S\),
若 \(R\) 是自反的,反对称的,传递的,那么 \(R\) 称为偏序关系。
性质定义
-
自反性
\(a \preceq a\),\(\forall a \in S\) -
反对称性
\(\forall a,b \in S\),若 \(a \preceq b\) 且 \(b \preceq a\),则 \(a = b\) -
传递性
\(\forall a,b,c \in P\),若 \(a \preceq b\) 且 \(b \preceq c\),则 \(a \preceq c\)
说明
- 按照定义,\(\leq\) 是一个典型的偏序关系。
- 在集合 \(S\) 中,若 \(a\)、\(b\) 存在偏序关系,则称它们为可比的,反之称为不可比的。
- 偏序集是由集合 \(S\) 与集合 \(S\) 上的偏序关系 \(R\) 构成的,记为 \((S, R)\)。
Dilworth定理
对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。
定义
设 \(C\) 是偏序集的一个子集:
- 如果 \(C\) 中元素相互可比,则称 \(C\) 为链。
- 如果 \(C\) 中元素相互不可比,则称 \(C\) 为反链。
好了,所以在本题中,我们令小于等于为一个偏序关系,则对这个关系的最小链划分的数量等于其最大反链长度,也就是其最长上升子序列的长度
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 200005
using namespace std;
int n;
int ma[maxn];
int fa[maxn];
int dp[maxn];
int fdp[maxn];
const int INF=1e9;
int main(){
while(cin>>ma[++n]){ }
n--;
for(int i=1;i<=n;i++){
fa[i]=ma[n+1-i];
}
memset(dp,INF,sizeof(dp));
memset(fdp,INF,sizeof(fdp));
fdp[1]=fa[1];
int len=1;
for(int i=2;i<=n;i++){
if(fa[i]>=fdp[len]){
fdp[++len]=fa[i];
continue;
}
int l=upper_bound(fdp+1,fdp+1+len,fa[i])-fdp;
fdp[l]=min(fdp[l],fa[i]);
}
cout<<len<<endl;
len=1;
dp[1]=ma[1];
for(int i=2;i<=n;i++){
if(dp[len]<ma[i]){
dp[++len]=ma[i];
continue;
}
int l=lower_bound(dp+1,dp+1+len,ma[i])-dp;
dp[l]=min(dp[l],ma[i]);
}
cout<<len<<endl;
system("pause");
return 0;
}
P2285 [HNOI2004] 打鼹鼠
题目描述
鼹鼠是一种很喜欢挖洞的动物,但每过一定的时间,它还是喜欢把头探出到地面上来透透气的。根据这个特点阿牛编写了一个打鼹鼠的游戏:在一个 \(n \times n\) 的网格中,在某些时刻鼹鼠会在某一个网格探出头来透透气。你可以控制一个机器人来打鼹鼠,如果 \(i\) 时刻鼹鼠在某个网格中出现,而机器人也处于同一网格的话,那么这个鼹鼠就会被机器人打死。而机器人每一时刻只能够移动一格或停留在原地不动。机器人的移动是指从当前所处的网格移向相邻的网格,即从坐标为 \((i, j)\) 的网格移向 \((i-1, j), (i+1, j), (i, j-1), (i, j+1)\) 四个网格,机器人不能走出整个 \(n \times n\) 的网格。游戏开始时,你可以自由选定机器人的初始位置。
现在知道在一段时间内,鼹鼠出现的时间和地点,请编写一个程序使机器人在这一段时间内打死尽可能多的鼹鼠。
输入格式
第一行为 \(n, m\)(\(n \le 1000\),\(m \le {10}^4\)),其中 \(m\) 表示在这一段时间内出现的鼹鼠的个数,接下来的 \(m\) 行中每行有三个数据 \(\mathit{time}, x, y\) 表示在游戏开始后 \(\mathit{time}\) 个时刻,在第 \(x\) 行第 \(y\) 个网格里出现了一只鼹鼠。\(\mathit{time}\) 按递增的顺序给出。注意同一时刻可能出现多只鼹鼠,但同一时刻同一地点只可能出现一只鼹鼠。
输出格式
仅包含一个正整数,表示被打死鼹鼠的最大数目。
输入输出样例 #1
输入 #1
2 2
1 1 1
2 2 2
输出 #1
1
解法&&个人感想
这道题是简单题,转移方程很容易想出来
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 1005
#define maxm 10005
using namespace std;
int n,m;
struct node{
int x,y,t;
}ma[maxm];
int dp[maxm];//也许,可以表示采了第i个之后(以第i个为结尾的最大数量)
int ans=0;
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>ma[i].t>>ma[i].x>>ma[i].y;
}
for(int i=1;i<=m;i++){
dp[i]=1;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=i-1;j++){
if(abs(ma[i].x-ma[j].x)+abs(ma[i].y-ma[j].y)<=abs(ma[i].t-ma[j].t)){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
for(int i=1;i<=m;i++){
ans=max(ans,dp[i]);
}
cout<<ans<<endl;
system("pause");
return 0;
}
后半学期,也请各位继续关注:
《我的青春线代物语果然有问题》
《高数女主养成计划》
《程设の旅》
《青春猪头少年不会梦到多智能体吃豆人》
《某Linux的开源软件》
《Charlotte太空探索》
还有——
《我的算法竞赛不可能这么可爱》
本期到此结束!

浙公网安备 33010602011771号