跨步DP
好心情
问题描述

思路分析
求出从起点走到终点的心情值的最小值 (min) 和最大值 (max) ,所有可能的心情一定在[min,max]区间之间。也就是说,所有可能的心情值一定是连续的。
证明:
假设求得最小值min ,最大值max,那么说明min是多选择了-1,或少选择了1,由于向前跳跃的步数是连续的1或2,对于多选择了-1,另一个比min大1的方案可以少选择(跨过-1),对于少选择了1,说明有1被跨过,另一个比min大1的方案可以选择不跨过那个1.
我们可以定义dp,记录到达i点的最小值与最大值,最后判断目标值tar是否在(dp[n].min,dp[n].max)区间即可。
代码
cpp
#include <iostream>
#include<vector>
using namespace std;
int main()
{
// 请在此输入您的代码
ios::sync_with_stdio(false);
cin.tie(0);
int n;cin>>n;
while(n--){
int s,tar;cin>>s>>tar;
vector<int>dat(s);
for(int i=0;i<s;i++) cin>>dat[i];
int dp_max1=0,dp_max2=0,p1;
int dp_min1=0,dp_min2=0,p2;
for(int i=0;i<s;i++){
p1=max(dp_max1,dp_max2)+dat[i]; dp_max1=dp_max2; dp_max2=p1; //更新最大值
p2=min(dp_min1,dp_min2)+dat[i]; dp_min1=dp_min2; dp_min2=p2; //更新最小值
}
p1=max(dp_max1,dp_max2);
p2=min(dp_min1,dp_min2);
if(tar<=p1&&tar>=p2) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
return 0;
}
打家劫舍
问题描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
代码
cpp
int rob(vector<int>& nums) {
int s=nums.size();
if(s==1) return nums[0];
int st=0;
int t=nums[0];
int p=0;
for(int i=1;i<s;i++){
p=max(t,st+nums[i]); //从上阶段最优解与上上阶段最优解+nums[i]中选择一个最大值作为当前最优解
st=t; //保存上阶段最优解
t=p; //保存当前阶段最优解
}
return p;
}
打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
思路分析
此问题与经典打家劫舍问题的唯一区别在于,房屋连成一个环。
将问题装换为经典打家劫舍问题即可,定义函数Maxtob,它可求房屋为直线排布下的从第start个房屋到第end个房屋能偷得的最大价值。
若从第0个房屋开始偷,则最后一个房屋必不能偷,求Maxtob(nums,0,s-1)
若不从第0个房屋开始偷,则最后一个房屋可以偷,求Maxtob(nums,1,s)
最后所求即为 max(Maxtob(nums,0,s-1),Maxtob(nums,1,s))
代码
cpp
int Maxtob(vector<int>&nums,int start,int end){
int st=0,t=nums[start],p=0;
for(int i=start+1;i<end;i++){
p=max(t,st+nums[i]);
st=t;
t=p;
}
return p;
}
int rob(vector<int>& nums) {
int s=nums.size();
if(s==1) return nums[0];
if(s==2) return max(nums[0],nums[1]);
return max(Maxtob(nums,0,s-1),Maxtob(nums,1,s));
}
咒语的最大总伤害
问题描述
一个魔法师有许多不同的咒语。
给你一个数组 power ,其中每个元素表示一个咒语的伤害值,可能会有多个咒语有相同的伤害值。
已知魔法师使用伤害值为 power[i] 的咒语时,他们就 不能 使用伤害为 power[i] - 2 ,power[i] - 1 ,power[i] + 1 或者 power[i] + 2 的咒语。
每个咒语最多只能被使用 一次 。
请你返回这个魔法师可以达到的伤害值之和的 最大值 。
思路分析
此问题是在值域上做打家劫舍,或者说在值域上做跨步。
将值映射为下标或者键,是解题的关键。
代码
cpp
long long maximumTotalDamage(vector<int>& power) {
unordered_map<int, int> cnt;
for (int x : power) {
cnt[x]++; //频数统计
}
vector<pair<int, int>> a(cnt.begin(), cnt.end());
sort(a.begin(),a.end());
int n = a.size();
vector<long long> f(n + 1); //f(i)表示前i个中选择咒语获得的最大伤害
for (int i = 0, j = 0; i < n; i++) {
auto& [x, c] = a[i];
while (a[j].first < x - 2) {
j++;
}//寻找第一个first值大于等于 x - 2的元素下标j [0,j-1]范围内获得的最大伤害值为f[j].
f[i + 1] = max(f[i], f[j] + (long long) x * c); //取第i个数与不取第i个数
}
return f[n];
}
对局匹配
蓝桥杯2017国赛题
网站中共有N名用户,他们的积分分别是A1,A2,A3...AN。请问最多有多少名用户同时在线且满足任意两名用户的积分差都不等于K?
思路分析
k=0,只需选择不相同的数即可,就是求n个数中有多少个不重复的数。k!=0,可以采用分组的方式,将n个人分为k组[0,k),对于第i组,组里的数就有{i,i+k,i+2k...},用val数组记录每个数具体有多少个。这样分组的目的就是使后续组与组之间选数互不影响(从a组中任选一个数t,与它相差k的数t-k和t+k都在同一组)。这样分完组后,就是在每个组中作独立的跨步dp了。
现在考虑子问题:对于一个组中的num个数,相邻的两个数不可选,问能选的数的总和最多是多少?
定义dp,dp[i]=max(dp[i-1],dp[i-2]+val[i])注意边界的特殊情况。
每个组选到的最大总和累加就是最终的答案。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int main() {
int n,k;
scanf("%d %d",&n,&k);
vector<int> a(n);
vector<int> cnt(N);
int mx = 0;
for(int i=0; i<n; i++) {
scanf("%d",&a[i]);
cnt[a[i]]++;
mx = max(mx,a[i]);
}
if(!k) {
int ans = 0;
for(int i=0; i<=N; i++) ans+=(cnt[i]!=0);
printf("%d",ans);
return 0;
}
int ans = 0;
for(int i=0; i<k; i++) {//分为k组
int num = 0;//第i组共有num种数
vector<int> val(N);//val[i]记录这组中第i个数有几个
vector<int> dp(N);
for(int j=i; j<=mx; j+=k) { //第i组中的数为{i,i+k,i+2k...}
val[num++] = cnt[j];
}
dp[0] = val[0];
for(int j=1; j<num; j++){
if(j == 1)dp[1] = max(val[0],val[1]);
else{
dp[j]=max(dp[j-1],dp[j-2]+val[j]);
}
}
ans += dp[num-1];
}
printf("%d",ans);
return 0;
}
总结:该解法巧妙地用分块的思想,将数据分成几个独立的块,每个块进行dp是互不干扰的。
能力爆表
问题描述
思路分析
定义dp dp[i][j]表示到达位置i能力为j(因为vi小于等于1000,当j大于1000时按1000算)所需最小花费。
当ans=min(dp[n][j]) (1<=j<=1000)仍为最大值N时,答案为-1;否则为ans。
代码
cpp
#include <iostream>
#include<vector>
using namespace std;
const long long N=0x3f3f3f3f3f3f3f3f;
void solve(){
int n; cin>>n;
vector<int>v(n+1);
vector<int>a(n+1);
vector<int>b(n+1);
vector<vector<long long>>dp(n+1,vector<long long>(1001,N));
for(int i=1;i<=n;i++) cin>>v[i];
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
dp[1][v[1]]=0;
for(int i=1;i<n;i++){
for(int j=0;j<=1000;j++){
if(dp[i][j]!=N){
if(j>=v[i+1]) dp[i+1][j]=min(dp[i+1][j],dp[i][j]); //更新dp[i+1][j]
int maxj=min(1000,j+b[i]);
if(maxj>=v[i+1])
dp[i+1][maxj]=min(dp[i+1][maxj],dp[i][j]+a[i]); //更新dp[i+1][maxj]
}
}
}
long long ans=N;
for(int i=1;i<=1000;i++){ //计算答案
ans=min(ans,dp[n][i]);
}
if(ans==N) cout<<-1<<endl; //dp[n]中所有元素未做更新,答案为-1
else cout<<ans<<endl;
}
int main()
{
// 请在此输入您的代码
ios::sync_with_stdio(0); cin.tie(0);
solve();
return 0;
}
