DP的优化
跳房子 单调队列优化,区间转移
题目描述
跳房子,也叫跳飞机,是一种世界性的儿童游戏,也是中国民间传统的体育游戏之一。跳房子的游戏规则如下:
在地面上确定一个起点,然后在起点右侧画 n 个格子,这些格子都在同一条直线上。每个格子内有一个数字(整数),表示到达这个格子能得到的分数。玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。第二次再从当前位置继续向右跳,依此类推。规则规定:玩家每次都必须跳到当前位置右侧的一个格子内。玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。
现在小 R 研发了一款弹跳机器人来参加这个游戏。但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 d。小 R 希望改进他的机器人,如果他花 g 个金币改进他的机器人,那么他的机器人灵活性就能增加 g,但是需要注意的是,每次弹跳的距离至少为 1。具体而言,当g < d时,他的机器人每次可以选择向右弹跳的距离为 d-g, d-g+1, d-g+2,…,d+g-2,d+g-1,d+g;否则(当g ≥ d时),他的机器人每次可以选择向右弹跳的距离为 1,2,3,…,d+g-2,d+g-1,d+g。
现在小 R 希望获得至少 k 分,请问他至少要花多少金币来改造他的机器人。
输入描述:
第一行三个正整数 n,d,k,分别表示格子的数目,改进前机器人弹跳的固定距离,以及希望至少获得的分数。相邻两个数之间用一个空格隔开。
接下来 n 行,每行两个正整数𝑥𝑖, 𝑠𝑖,分别表示起点到第i个格子的距离以及第i个格子的分数。两个数之间用一个空格隔开。保证𝑥𝑖按递增顺序输入。
ps:本题共10组测试数据,每组数据10分。对于全部的数据满足1≤n≤500000,1≤d≤2000,1≤xi,k≤109,|si|≤105。
输出描述:
共一行,一个整数,表示至少要花多少金币来改造他的机器人。若无论如何他都无法获得至少k 分,输出-1。
示例1
输入
7 4 10
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
输出
2
说明
花费 2 个金币改进后,小 R 的机器人依次选择的向右弹跳的距离分别为 2,3,5,3,4, 3,先后到达的位置分别为 2,5,10,13,17,20,对应 1, 2, 3, 5, 6, 7 这 6 个格子。这些格子中的数字之和 15 即为小 R 获得的分数。
示例2
输入
7 4 20
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
输出
-1
说明
由于样例中 7 个格子组合的最大可能数字之和只有 18 ,无论如何都无法获得 20 分。
思路
我们可以很快想到二分答案。然后是一个常规的DP。
:跳到第i个格子的最大分数。
对于
如果枚举k。复杂度O(n * x[i] * logx[i])
如果不枚举k,我们可以维护一个单调队列。O(1)转移。
时间复杂度O(n*logx[i])
#include<bits/stdc++.h>
#define LL long long
#define pii pair<int, int>
using namespace std;
int x[500005], s[500005];
LL f[500005];
int q[500005];
int n, d, k;
int ok(int g){
int posL=1, posR=0, pos=0;
memset(f, 0x8f, sizeof(f));
f[0]=0;
for(int i=1; i<=n; i++){
while(posL<=posR&&x[i]-x[q[posL]]>d+g){//不合法弹出
posL++;
}
while(pos<i&&x[i]-x[pos]>=d-g){//可能加入
if(x[i]-x[pos]>d+g){//超出区间距离
pos++; continue;
}
while(posL<=posR&&f[q[posR]]<=f[pos]){
posR--;
}
q[++posR]=pos++;
}
if(posL>posR) continue;
f[i]=max(f[i], f[q[posL]]+s[i]);
}
return *max_element(f+1, f+1+n)>=k;
}
int main() {
scanf("%d%d%d", &n, &d, &k);
int sum=0;
for(int i=1; i<=n; i++){
scanf("%d%d", &x[i], &s[i]);
}
int l=0, r=1e9+10, ans=-1;
while(l<=r){
int mid=l+r>>1;
if(ok(mid)){
ans=mid;
r=mid-1;
}
else{
l=mid+1;
}
}
printf("%d\n", ans);
return 0;
}
子串(状态计数重用)
题目描述
有两个仅包含小写英文字母的字符串 A 和 B。现在要从字符串 A 中取出 k 个互不重叠的非空子串,然后把这 k 个子串按照其在字符串 A 中出现的顺序依次连接起来得到一个新的字符串,请问有多少种方案可以使得这个新串与字符串 B 相等?注意:子串取出的位置不同也认为是不同的方案。
输入描述:
第一行是三个正整数 n,m,k,分别表示字符串 A 的长度,字符串 B 的长度,以及问题描述中所提到的 k,每两个整数之间用一个空格隔开。
第二行包含一个长度为 n 的字符串,表示字符串 A。
第三行包含一个长度为 m 的字符串,表示字符串 B。
输出描述:
输出共一行,包含一个整数,表示所求方案数。由于答案可能很大,所以这里要求输出答案对 1,000,000,007 取模的结果。
示例1
输入
6 3 1
aabaab
aab
输出
2
说明
样例 1所有合法方案如下:(加下划线的部分表示取出的子串)
示例2
输入
6 3 2
aabaab
aab
输出
7
说明
样例 2:所有合法方案如下:(加下划线的部分表示取出的子串)
示例3
输入
6 3 3
aabaab
aab
输出
7
说明
样例 3所有合法方案如下:(加下划线的部分表示取出的子串)
备注:
对于第 1 组数据:1≤n≤500,1≤m≤50,k=1;
对于第 2 组至第 3 组数据:1≤n≤500,1≤m≤50,k=2;
对于第 4 组至第 5 组数据:1≤n≤500,1≤m≤50,k=m;
对于第 1 组至第 7 组数据:1≤n≤500,1≤m≤50,1≤k≤m;
对于第 1 组至第 9 组数据:1≤n≤1000,1≤m≤100,1≤k≤m;
对于所有 10 组数据:1≤n≤1000,1≤m≤200,1≤k≤m。
思路
思路一:
我们很容易想到一个状态
初始化:;
转移
1.A[i]单独作为一段。
2.A[i]和前面的一起组成一段:
这个地方必须优化:我们考虑,当A[i]和前面组成一起的时候,那么和i-1组成k段,并且A[i-1]必须和B[j-1]是匹配。这样A[i]和A[i-1]连接
在一起就满足了。
所以:A[i]和前面的一起组成一段:f[i][j][k]+=f[i-1[j-1][k]-f[i-2][j][k](因为状态表示时A[i]可选可不选 这个容斥就是A[i-1]必须和B[j-1]是匹配)
思路二:为什么要容斥,因为状态表示时A[i]可选可不选。那么我们加一维表示A[i]是否选择匹配就可以了。
转移:A[i]==B[j]:
转移:A[i]!=B[j]:
思路一:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int f[2][1005][205];
char A[1005], B[1005];
const int mod=1e9+7;
int main() {
int n, m, posK; scanf("%d%d%d", &n, &m, &posK);
scanf("%s", A+1);
scanf("%s", B+1);
for(int i=0; i<=n; i++){
f[0][i][0]=1;
}
for(int k=1; k<=posK; k++){
memset(f[k%2], 0, sizeof(f[k%2]));
for(int i=1; i<=n; i++){
for(int j=1; j<=min(m, i); j++){
f[k%2][i][j]=f[k%2][i-1][j];
if(A[i]==B[j]){
f[k%2][i][j]+=(f[k%2][i-1][j-1]+f[(k-1)%2][i-1][j-1])%mod;
f[k%2][i][j]%=mod;
if(i>1){
f[k%2][i][j]=(f[k%2][i][j]-f[k%2][i-2][j-1]+mod)%mod;
}
}
//cout<<i<<" "<<j<<" "<<k<<" "<<f[i][j][k]<<endl;
}
}
}
printf("%d\n", f[posK%2][n][m]);
return 0;
}
思路二:
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int f[2][1005][205][2];
char A[1005], B[1005];
const int mod=1e9+7;
int main() {
int n, m, posK; scanf("%d%d%d", &n, &m, &posK);
scanf("%s", A+1);
scanf("%s", B+1);
for(int i=0; i<=n; i++){
f[0][i][0][0]=1;
}
for(int k=1; k<=posK; k++){
memset(f[k%2], 0, sizeof(f[k%2]));
for(int i=1; i<=n; i++){
for(int j=1; j<=min(m, i); j++){
if(A[i]==B[j]){
f[k%2][i][j][1]=(f[(k-1)%2][i-1][j-1][1]+f[(k-1)%2][i-1][j-1][0])%mod;
f[k%2][i][j][1]+=(f[k%2][i-1][j-1][1]);
f[k%2][i][j][1]%=mod;
f[k%2][i][j][0]=(f[k%2][i-1][j][1]+f[k%2][i-1][j][0])%mod;
}
else{
f[k%2][i][j][1]=0;
f[k%2][i][j][0]=(f[k%2][i-1][j][1]+f[k%2][i-1][j][0])%mod;
}
}
}
}
printf("%d\n", (f[posK%2][n][m][0]+f[posK%2][n][m][1])%mod);
return 0;
}
飞扬的小鸟 完全背包减少枚举k
题目描述
为了简化问题,我们对游戏规则进行了简化和改编:
-
游戏界面是一个长为n,高 为m的二维平面,其中有k个管道(忽略管道的宽度)。
-
小鸟始终在游戏界面内移动。小鸟从游戏界面最左边任意整数高度位置出发,到达游戏界面最右边时,游戏完成。
-
小鸟每个单位时间沿横坐标方向右移的距离为1,竖直移动的距离由玩家控制。如果点击屏幕,小鸟就会上升一定高度X,每个单位时间可以点击多次,效果叠加;如果不点击屏幕,小鸟就会下降一定高度Y。小鸟位于横坐标方向不同位置时,上升的高度X和下降的高度Y可能互不相同。
-
小鸟高度等于0或者小鸟碰到管道时,游戏失败 。小鸟高度为m时,无法再上升。
现在,请你判断是否可以完成游戏。如果可以,输出最少点击屏幕数;否则,输出小鸟最多可以通过多少个管道缝隙。
输入描述:
第1行有3个整数n,m,k,分别表示游戏界面的长度,高度和水管的数量,每两个整数之间用一个空格隔开;
接下来的n行,每行2个用一个空格隔开的整数X和Y,依次表示在横坐标位置0~n-1上玩家点击屏幕后,小鸟在下一位置上升的高度X,以及在这个位置上玩家不点击屏幕时,小鸟在下一位置下降的高度Y。
接下来k行,每行3个整数P,L,H,每两个整数之间用一个空格隔开。每行表示一个管道,其中P表示管道的横坐标,L表示此管道缝隙的下边沿高度为L,H表示管道缝隙上边沿的高度(输入数据保证P各不相同,但不保证按照大小顺序给出)。
输出描述:
第一行,包含一个整数,如果可以成功完成游戏,则输出1,否则输出0。
第二行,包含一个整数,如果第一行为1,则输出成功完成游戏需要最少点击屏幕数,否则,输出小鸟最多可以通过多少个管道缝隙。
示例1
输入
10 10 6
3 9
9 9
1 2
1 3
1 2
1 1
2 1
2 1
1 6
2 2
1 2 7
5 1 5
6 3 5
7 5 8
8 7 9
9 1 3
输出
1
6
说明
如下图所示,蓝色直线表示小鸟的飞行轨迹,红色直线表示管道。
示例2
输入
10 10 4
1 2
3 1
2 2
1 8
1 8
3 2
2 1
2 1
2 2
1 2
1 0 2
6 7 9
9 1 4
3 8 10
输出
0
3
说明
如下图所示,蓝色直线表示小鸟的飞行轨迹,红色直线表示管道。
备注:
对于30%的数据:5≤n≤10,5≤m≤10,k=0,保证存在一组最优解使得同一单位时间最多点击屏幕3次;
对于50%的数据:5≤n≤20,5≤m≤10,保证存在一组最优解使得同一单位时间最多点击屏幕3次;
对于70%的数据:5≤n≤1000,5≤m≤100;
对于100%的数据:5≤n≤10000,5≤m≤1000,0≤k
思路
很容易想到状态方程:
第一个下降,第二个方程是上升,k是枚举点击了几次屏幕。并且我们知道一秒只能升或者降.
这两个没有在一起出现,我们分开维护。前面是一个01背包。后面是一个完全背包。这样就可以不枚举k。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int x[10005], y[10005];
int L[10005], R[10005];
int f[10005][3005];
bool ff[10005];
int main() {
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
for(int i=0; i<=n; i++) {
R[i]=m+1;
}
for(int i=1; i<=n; i++) {
scanf("%d%d", &x[i], &y[i]);
}
for(int i=1; i<=k; i++) {
int x;
scanf("%d", &x);
ff[x]=1;
scanf("%d%d", &L[x], &R[x]);
}
memset(f, 0x3f, sizeof(f));
for(int i=L[0]+1; i<R[0]; i++) {
f[0][i]=0;
}
for(int i=1; i<=n; i++) {
//维护上升 完全背包
for(int j=x[i]+1; j<=3000; j++) {
f[i][j]=min(f[i-1][j-x[i]]+1, f[i][j-x[i]]+1);
}
//维护下降 01背包
for(int j=m-y[i]; j>=1; j--) {
f[i][j]=min(f[i][j], f[i-1][j+y[i]]);
}
//最高为m
for(int j=3000; j>=m; j--) {
f[i][m]=min(f[i][m], f[i][j]);
}
//水管限制
for(int j=0; j<=L[i]; j++) {
f[i][j]=f[0][0];
}
//水管限制
for(int j=3000; j>=R[i]; j--) {
f[i][j]=f[0][0];
}
}
int ans=f[0][0];
for(int i=L[n]+1; i<R[n]; i++) {
ans=min(ans, f[n][i]);
}
if(ans==f[0][0]){
int res=0;
for(int i=1; i<=n; i++){
int ok=0;
for(int j=L[i]+1; j<R[i]; j++){
if(f[i][j]<f[0][0]){
ok=1; break;
}
}
if(!ok) break;
if(ff[i]){
res++;
}
}
printf("0\n%d\n", res);
}
else{
printf("1\n%d\n", ans);
}
return 0;
}
[ZJOI2004]午餐 双塔DP 贪心证明+前缀和降维
https://www.luogu.com.cn/problem/P2577
题目描述
上午的训练结束了,THU ACM小组集体去吃午餐,他们一行N人来到了著名的十食堂。这里有两个打饭的窗口,每个窗口同一时刻只能给一个人打饭。由于每个人的口味(以及胃口)不同,所以他们要吃的菜各有不同,打饭所要花费的时间是因人而异的。另外每个人吃饭的速度也不尽相同,所以吃饭花费的时间也是可能有所不同的。
THU ACM小组的吃饭计划是这样的:先把所有的人分成两队,并安排好每队中各人的排列顺序,然后一号队伍到一号窗口去排队打饭,二号队伍到二号窗口去排队打饭。每个人打完饭后立刻开始吃,所有人都吃完饭后立刻集合去六教地下室进行下午的训练。
现在给定了每个人的打饭时间和吃饭时间,要求安排一种最佳的分队和排队方案使得所有人都吃完饭的时间尽量早。
假设THU ACM小组在时刻0到达十食堂,而且食堂里面没有其他吃饭的同学(只有打饭的师傅)。每个人必须而且只能被分在一个队伍里。两个窗口是并行操作互不影响的,而且每个人打饭的时间是和窗口无关的,打完饭之后立刻就开始吃饭,中间没有延迟。
现在给定N个人各自的打饭时间和吃饭时间,要求输出最佳方案下所有人吃完饭的时刻。
输入格式
第一行一个整数N,代表总共有N个人。
以下N行,每行两个整数 Ai,Bi。依次代表第i个人的打饭时间和吃饭时间。
输出格式
一个整数T,代表所有人吃完饭的最早时刻。
输入
5
2 2
7 7
1 3
6 4
8 5
输出
17
说明/提示
所有输入数据均为不超过200的正整数。
思路
首先我们可以得到一个
对于i我们枚举他在哪个队列就可以了。
因为j,k<=40000。所以这个方程必须降维。我们发现j+k=前i个学生的打饭时间和。
所以可以降一维。
题目没有排队是顺序是输入的顺序,例如:
9 1
1 10
1,2在一个队
如果1先打饭,最晚时间是20
如果1先打饭,最晚时间是11
所以应该贪心排序才能DP。
应该按什么顺序?
我们假设两个人a和b
如果a在前面
如果b在前面
什么时候T1<T2?。
我们枚举4个情况
a.x<b.x
a.x>b.x
a.y<b.y
a.y>b.y
我们分析第4个:a.y>b.y
不能确定。
我们假设:。那么T2>T1显然
我们假设:。可以得到。
因为a.y>b.y。所以T2>T1也成立。
所以按a.y>b.y排序。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
struct Node{
int x, y;
}a[205];
int f[205][40005];
int main() {
int n; scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%d%d", &a[i].x, &a[i].y);
}
sort(a+1, a+1+n, [](Node &a, Node &b){return a.y>b.y;});
memset(f, 0x3f, sizeof(f));
f[0][0]=0;
int sum=0;
for(int i=1; i<=n; i++){
sum+=a[i].x;
for(int j=0; j<=sum; j++){
int k=sum-j;
if(k>=0){
int res=0x3f3f3f3f;
if(j-a[i].x>=0){
res=max(f[i-1][j-a[i].x], j+a[i].y);
}
if(k-a[i].x>=0){
res=min(res, max(f[i-1][j], k+a[i].y));
}
f[i][j]=min(res, f[i][j]);
}
}
}
int ans=0x3f3f3f3f;
for(int i=0; i<=sum; i++){
ans=min(ans, f[n][i]);
}
printf("%d\n", ans);
return 0;
}
最长波浪型的子序列
题目描述
花匠栋栋种了一排花,每株花都有自己的高度。花儿越长越大,也越来越挤。栋栋决定把这排中的一部分花移走,将剩下的留在原地,使得剩下的花能有空间长大,同时,栋栋希望剩下的花排列得比较别致。
具体而言,栋栋的花的高度可以看成一列整数
。设当一部分花被移走后,剩下的花的高度依次为,则栋栋希望下面两个条件中至少有一个满足:
条件A:对于所有
条件 BB:对于所有
注意上面两个条件在m=1时同时满足,当m > 1时最多有一个能满足。
请问,栋栋最多能将多少株花留在原地。
输入格式
第一行包含一个整数nn,表示开始时花的株数。
第二行包含nn个整数,依次为
,表示每株花的高度。
输出格式
一个整数m,表示最多能留在原地的花的株数。
输入
5
5 3 2 1 2
输出
3
说明/提示
【输入输出样例说明】
有多种方法可以正好保留 3 株花,例如,留下第 1、4、5 株,高度分别为 5、1、2,满足条件 B。
【数据范围】
对于 20%的数据,
对于 30%的数据,
对于 70%的数据,
对于 100%的数据,
随机生成,所有随机数服从某区间内的均匀分布。
思路
这题显然是求一个最长波浪型的子序列,然后我们可以发现,一个连续的上升或下降序列中,有且只有一个能被选入最终的子序列中,不然就不满足波浪的需求,因此我们只需要统计连续的上升与下降序列的个数,最终的答案等于统计数+1。
另外,因为是要求严格波浪,所以对于相等的连续序列我们可以直接忽略,相当于只有一个点。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int h[100005];
int main() {
int n; scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%d", &h[i]);
}
if(n==1){
printf("%d\n", 1);
}
else{
int op=0;
int ans=1;
for(int i=2; i<=n; i++){
if(h[i]==h[i-1]) continue;
if(op!=1&&h[i]>h[i-1]) ans++, op=1;
else if(op!=2&&h[i]<h[i-1]) ans++, op=2;
}
printf("%d\n", ans);
}
return 0;
}
选择客栈 枚举计数
https://ac.nowcoder.com/acm/problem/16594
题目描述
丽江河边有n家很有特色的客栈,客栈按照其位置顺序从1到n编号。每家客栈都按照某一种色调进行装饰(总共k种,用整数 0~k-1表示),且每家客栈都设有一家咖啡店,每家咖啡店均有各自的最低消费。
两位游客一起去丽江旅游,他们喜欢相同的色调,又想尝试两个不同的客栈,因此决定分别住在色调相同的两家客栈中。晚上,他们打算选择一家咖啡店喝咖啡,要求咖啡店位于两人住的两家客栈之间(包括他们住的客栈),且咖啡店的最低消费不超过p。
他们想知道总共有多少种选择住宿的方案,保证晚上可以找到一家最低消费不超过p元的咖啡店小聚。
输入描述:
第一行三个整数 n,k,p,每两个整数之间用一个空格隔开,分别表示客栈的个数,色调的数目和能接受的最低消费的最高值;
接下来的n行,第i+1行两个整数,之间用一个空格隔开,分别表示i号客栈的装饰色调和i号客栈的咖啡店的最低消费。
输出描述:
输出只有一行,一个整数,表示可选的住宿方案的总数。
示例1
输入
5 2 3
0 5
1 3
0 2
1 4
1 5
输出
3
说明
2人要住同样色调的客栈,所有可选的住宿方案包括:住客栈①③,②④,②⑤,④⑤,但是若选择住4、5号客栈的话,4、5号客栈之间的咖啡店的最低消费是4,而两人能承受的最低消费是3元,所以不满足要求。因此只有前3种方案可选。
备注:
对于30%的数据,有 n ≤ 100;
对于50%的数据,有 n ≤ 1,000;
对于100%的数据,有 2 ≤ n ≤ 200,000,0 < k ≤ 50,0 ≤ p ≤ 100,0 ≤ 最低消费 ≤ 100。
思路:枚举第第二个客栈,然后记录上一个<=p客栈的位置pos。在1-pos的前缀和,和1-i的前缀和就可以了。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
int last[55], sum[55], cnt[55], pos=0;
int main() {
int n, k, p; scanf("%d%d%d", &n, &k, &p);
LL ans=0;
for(int i=1; i<=n; i++){
int x, y; scanf("%d%d", &x, &y);
if(y<=p){
pos=i;
}
if(pos>=last[x]){//前面有一个<=p。更新
sum[x]=cnt[x];
}
ans+=sum[x];
cnt[x]++;
last[x]=i;
}
printf("%lld\n", ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)