暑期强化DP学习的一个计划,每天刷点cf,每日赛过活神仙(bushi)
1.CF_1660C(1300)
tag -> greddy dp string
题目大意:
现在给定一个概念:偶数字符串:只要满足字符串长度为偶数并且每一个奇数i满足a[i]==a[i+1]。
题目要求对于一个字符串,最少删除多少个字符可以得到一个偶数字符串,删除的位置任意,不一定连续,不改变原来相对位置。
思路:
直接for循环遍历,如果遍历到i,发现a[i]已经存在且上次存在的位置为j,那么子串[j,i]就可以贡献出长度为2的偶数字符串,利用补集思想,求出可以得到的最长的偶数字符串,用原长度-最长偶数字符串长度即可得到答案。
贪心策略证明:
对于一段字符串aoeoaee,刚开始枚举时aoe均未出现过,不可以贡献结果,当第一次出现第二个o的时候,发现oeo可以把e给去掉组成一个偶数字符串,此时得到ooaee,(因为前面的a肯定也没有办法再组成偶数字符串了,题目要求从任意位置删除字符,并不可以改变原来字符的相对位置),再往后遍历a只出现了一次,e出现了一次,当e出现第二次的时候又贡献出ee的一个偶数字符串,此时只出现一次的a也无法再与别的组成偶数字符串,因此最终可以构建出ooee的偶数字符串,那么需要去除的长度为7-4=3
对于字符串aoeoaee也可以构建出来aaee的偶数字符串,但是我们最终并不是求有多少种偶数字符串的构建方法,只是求一个满足条件的最长的偶数字符串长度,因此该贪心策略满足。
2.CF_1631B(1100)
点击此处直达
tag -> greedy dp
题目大意:
给定一个长度为 n n n 的数据 a a a。你可以执行若干次操作,每次操作你可以选择一个位置 i i i 和一个整数 k k k,但需要保证 1 ⩽ l ⩽ l + 2 ⋅ k − 1 ⩽ n 1\leqslant l\leqslant l+2\cdot k-1\leqslant n 1⩽l⩽l+2⋅k−1⩽n, k ⩾ 1 k\geqslant 1 k⩾1。然后对于所有的 0 ⩽ j ⩽ k − 1 0\leqslant j\leqslant k-1 0⩽j⩽k−1,将 a i + j a_{i+j} ai+j 的值替换为 a i + k + j a_{i+k+j} ai+k+j。你希望最终得到一个所有元素的值相等的数组,求最小的操作次数。
思路:
由题目我们可以得到一个性质:最终的序列一定为全部由a[n]组成的序列,所以我们只需要从后往前遍历,一旦发现一个不等于a[n]的数,然后用后面最长的连续的a[n]序列去覆盖,然后统计即可。
初做题的误区,利用双指针+倍增,每次覆盖2^k长度的区间进行统计,然而并不是最优。
比如1 1 1 4 4 4 4 4 1 4 1 4 1 4
如果按照2^k倍增,应该是
1 1 1 4 4 4 4 4 1 4 1 4 4 4
1 1 1 4 4 4 4 4 1 4 4 4 4 4
1 1 1 4 4 4 4 4 4 4 4 4 4 4
4 4 4 4 4 4 4 4 4 4 4 4 4 4
一共四步
然而正解应该是
1 1 1 4 4 4 4 4 1 4 1 4 4 4
1 1 1 4 4 4 4 4 4 4 4 4 4 4
4 4 4 4 4 4 4 4 4 4 4 4 4 4
一共三步
3.CF_1703G
点击此处直达
tag -> dp , greedy , brute force , bitmasks
题目大意:
有 n 个箱子需要按顺序打开,第 i 个箱子中有 ai个金币,可以用好钥匙和坏钥匙去开箱子,好钥匙需要花费 k 个金币,坏钥匙不花费金币,但是会使当前这个箱子开始到第 n 个箱子中的金币全部减半。开箱的过程中金币可以是负数的,即可以赊账,问最多能留下多少硬币。
思路:
看到此题的时候,老贪心侠的毛病又犯了,直接想了一个贪心策略:如果好钥匙开可以赚钱那就用好钥匙开,不然就用坏钥匙开。
然而显然肯定是不可以的,因为可能用坏钥匙开造成的利益损失可能远大于好钥匙开的短暂的小的损失,比如如果好钥匙开箱子的钱是5块,现在又6 4 18 20四个箱子,按照贪心策略,应该是:
6-5 4/2 18/2-5 20/1-5 加起来是12
然而如果全部用好钥匙开,那么就是
6-5 4-5 19-5 20-5 加起来是28远大于12,所以贪心策略失败,不可以仅仅考虑当前局部的最优化利益
贪心不行就得往dp方面去思考了,我们对于每一个物品都有两个选择,要么用好钥匙开,要么用坏钥匙开,那么我们完全可以把所有的情况枚举出来,进行判断。然而O(2n)的时间复杂度过于可怕,所以我们使用dp递推式,我们知道a[i]最大是232所以最多需要考虑使用33次坏钥匙的情况,因为33次坏钥匙之后,你再怎么使用坏钥匙,那对最终的结果都不会产生任何影响了,因为任何一个数经够32次/2都会变成0,对答案没有正向贡献。
方程概念:定义f[i][j]表示前i个巷子用了j个坏钥匙开所得到的最大的收益。
决策:
对于第i个物品,有两个决策,分别是用好钥匙开,和用坏钥匙开
状态转移:
如果第i个物品用好钥匙开,那么就从前i-1个物品用j个坏钥匙开转移过来;
如果第i个物品用坏钥匙开,那么就从前i-1个物品用j-1个坏钥匙开转移过来;
所以状态转移方程式为:f[i][j]=max(f[i-1][j]+(a[i]>>j)+k,f[i-1][j-1]+(a[i]>>j))
本题相当于一个背包变形的一个01决策问题,解题的关键在于看出33次是坏钥匙的最大限制,后面对结果都没有正向贡献所以不予以考虑。
参考代码:
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<map>
using namespace std;
typedef long long ll;
const int MAXN = 2e5 + 7;
ll a[MAXN];
ll f[MAXN][35];
void init(int n)
{
for(int i=0;i<=n+1;i++)
{
for(int j=0;j<=34;j++)
{
f[i][j]=-1e18;
}
}
for(int i=0;i<=n+1;i++)
a[i]=0;
}
int main()
{
int t;
cin>>t;
while(t--)
{
int n,k;
cin>>n>>k;
init(n);
for(int i=1;i<=n;i++)
cin>>a[i];
f[0][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=33&&j<=i;j++)
{
f[i][j]=max(f[i][j],f[i-1][j]+(ll)((a[i]>>j)-k));//第i个用好钥匙开
if(j!=0)
f[i][j]=max(f[i][j],f[i-1][j-1]+(ll)((a[i]>>j)));//第i个用坏钥匙开的
}
f[i][33]=max(f[i][33],f[i-1][33]);//不能忘了把所有坏钥匙全用了的情况
}
ll res=-1e18;
for(int i=0;i<=33;i++)
res=max(res,f[n][i]);
cout<<res<<endl;
}
}
4.CF_1618D(1300)
点此处直达
tag -> greedy , dp
备注:纯纯的一个贪心思维题
题目大意:
给定 n n n 个数和一个数 k k k,要求对这 n n n 个数进行 k k k 次操作,每次操作取出两个数 a i , a j ( i ≠ j ) a_i,a_j (i\neq j) ai,aj(i=j),并将 ⌊ a i a j ⌋ \lfloor \frac{a_i}{a_j} \rfloor ⌊ajai⌋ 加进得分中,其中 ⌊ x y ⌋ \lfloor \frac{x}{y} \rfloor ⌊yx⌋ 为不超过 x y \frac{x}{y} yx 的最大整数。 k k k 次操作后,将剩下的 n − 2 k n-2k n−2k 个数直接加入到得分中。求最终得分的最小值。
思路:
我们任选两个数得到的最小增益要么是0要么是1(相同为0,相同为1)
题目要求最终剩下的数字和也要尽可能小,所以首先需要排序预处理,所以我们任务很明确就是对于序列中后2k的元素进行选取,使得每次选取的两个元素尽可能不一样,由于我们的序列是排序过的,如果按照相邻选取则可以达到尽可能多选一样的元素的效果,我们要求不一样,所以可以间隔着选取,若一共选2k个元素,那么第一个就和第k+1个作为1组,第2个就和第k+2个作为1组,贪心策略成立。
5.CF_1633D(1600)
点击此处直达
tag ->dp , greedy
备注:01背包变形,思路还挺好想的
题目大意:
你有一个长度为
n
n
n,初始全为
1
1
1 的数组
a
a
a,和两个长度为
n
n
n 的数组
b
,
c
b,c
b,c。
你可以最多进行
k
k
k 次如下的操作:选择两个正整数
i
,
x
i,x
i,x,使
a
i
a_{i}
ai 变成
(
a
i
+
⌊
a
i
x
⌋
)
\left ( a_{i}+\left \lfloor \dfrac{a_{i}}{x}\right \rfloor \right )
(ai+⌊xai⌋)。
最后,如果 a i = b i a_{i}=b_{i} ai=bi,你将会获得 c i c_{i} ci 的收益。
最大化总收益。
思路:
常规想法:由于最多可以进行k次增值操作,每一个物品增值到所需要的数值所需要的操作数是不一样的,所以我们要尽可能地用最少地操作数去获得最多的价值,显然用dp,那么对于每一个物品都有取和不取两个决策,如果这个物品取,则在当前状态需要消耗t[b[i]]次操作数,如果不取则不需要消耗操作数。
本题题目很清楚,给定了最多操作的次数,并且是求有限次数操作下的最大收益,我们如果把操作次数当作物品的重量,本题就自然而然变成了01背包问题。
变量定义:f[j]:消耗j个操作数得到的最大收益
最终所求f[0…k]的最大值
状态转移:01背包的方程(f[j]=max(f[j],f[j-t[b[i]]+c[i]))
本题需要一个初始化操作,即求t[0…1000],暴力即可,时间复杂度106.
参考代码:
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<map>
using namespace std;
typedef long long ll;
const int MAXN = 1e3+ 7;
const int MAXM=1e6+7;
int b[MAXN],c[MAXN],f[MAXM],t[MAXM];
void init(){
for(int i = 1 ; i <= 2000 ; i++)
t[i] = 1e9;
t[1] = 0;
for(int i = 1 ; i <= 2000 ; i++)
for(int j = 1 ; j <= i ; j++)
t[i + i / j] = min(t[i + i / j], t[i] + 1);
}
int main()
{
init();
int T;
cin>>T;
while(T--)
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>b[i];
for(int i=1;i<=n;i++)
cin>>c[i];
memset(f,0,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
{
for(int j=k;j>=t[b[i]];j--)
{
f[j]=max(f[j],f[j-t[b[i]]]+c[i]);
}
}
int res=0;
for(int i=0;i<=k;i++)
res=max(res,f[i]);
cout<<res<<endl;
}
}
6.CF_1644C(1400)
点击此处直达
tag -> brute force , greedy , dp
备注:一个很好的线性思维dp题
题目大意:
给定一个序列 { a n } \left\{a_n\right\} {an} 与 整数 x x x
定义 f ( k ) f(k) f(k) 表示经过如下操作后, 序列 a a a 中最大的连续子段和: 将 a a a 中 k k k 个不同的位置上的数加上 x x x
请求出 f ( k ) , k ∈ [ 0 , n ] f(k),\ k\in[0, n] f(k), k∈[0,n]
思路:
对于本题,要求操作之后的最大连续子段和,我们首先能想到的一个贪心策略是,对于想要使k个元素分别加x,并且让最后的最大连续子段和最大,那么肯定把这k个元素加在原本身的长度大于等于k的最大连续子段和上,这样才会让最后结果货真价实地增加k*x,所以我们首先需要利用dp求出来长度为[0…n]的最大子段和分别为多少,然后再在其基础上加上k*x即可
本题出现了单一变量复用的情况。
一开始f[i]表示长度为i的最大连续子段和
到后面f[i]转义表示长度大于等于i的最大连续子段和
参考代码:
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<map>
using namespace std;
typedef long long ll;
const int MAXN = 2e5 + 7;
int a[MAXN],f[MAXN],dp[MAXN];
//dp[i]表示增加长度为i的最大连续字段和时候所获得的总和
int main()
{
int t;
cin>>t;
while(t--)
{
int n,x;
cin>>n>>x;
memset(a,0,sizeof(a));
memset(f,0,sizeof(f));
memset(dp,-0x3f,sizeof(dp));
for(int i=1;i<=n;i++)
{
cin>>a[i];
f[i]=f[i-1]+a[i];
}
for(int i=1;i<=n;i++)
{
for(int j=i;j<=n;j++)
{
dp[j-i+1]=max(dp[j-i+1],f[j]-f[i-1]);//状态转移求长度为j-i+1的最大连续字段和
}
}
dp[0]=max(dp[0],0);
for(int i=n-1;i>=0;i--)
dp[i]=max(dp[i],dp[i+1]);//f[i]改变含义,转为求长度大于等于i的最大连续子段和
for(int k=1;k<=n;k++)
dp[k]=max(dp[k-1],dp[k]+k*x);//统计最后答案
for(int i=0;i<=n;i++)
cout<<dp[i]<<" ";
cout<<endl;
}
}
7.CF_1553C(1200)
点击此处直达
tag -> bitmasks , brute force , dp , greedy
标签:贪心思维题,动脑子
题目大意:
现在一共有 10 10 10 次罚球机会,其中奇数次属于队伍 1 1 1,偶数次属于队伍 2 2 2。
在罚球开始前,你预先知道了每一次罚球是必定成功(字符 1 1 1),必定不成功(字符 0 0 0),还是由你操纵(字符 ? ? ?)。当一次罚球成功时,所在的队伍就会得到 1 1 1 分,最终得分高的队伍获胜。
当裁判发现某个队伍必胜的时候,就会宣布停止罚球(注意,裁判并不能预知罚球结果)。现在,你希望罚球的轮数尽可能少,求出这个轮数。
思路:
什么时候可以确定后面不需要比了?
当前某一个队伍已经获得的分数+剩下还没获得的最大可以获得的分数<另一个队伍已经获得的分数
所以我们只需要利用贪心策略:压倒性支持某一个队伍,只要是?而且是我们支持的队伍,那就一定让他赢,只要是?但是不是我们支持的队伍,那就一定让他输!
当然我们采用墙根草战术,两个都支持一下,看看两个队伍的表现怎么样,支持谁结束比赛的轮数更少那就支持谁。
核心代码:
while(t--){
cin>>s;
cout<<min(cal1(),cal2())<<endl;
}
//cal1()是全力支持第一个队伍最快能结束的时间
//cal2()是全力支持第二个队伍最快能结束的时间
8.CF_1706B(1100)
点击此处直达
tag -> dp , greedy , math
标签:dp,规律
题目大意:
你站在一个无限大的网格中,初始时在 (0,0)(0,0),并将 (0,0)(0,0) 涂上颜色 c i c_i ci。你要游走 n n n 次,第 i i i 次游走可以前往 ( x + 1 , y ) (x+1,y) (x+1,y), ( x − 1 , y ) (x-1,y) (x−1,y), ( x , y + 1 ) (x,y+1) (x,y+1),要求目的地不能已经被涂色,并且在那个位置涂上颜色 。一个“塔”是指 x x x 相同, y y y 连续的一段相同颜色格子,对于每个 i i i 独立求出颜色 i i i 的塔的可能最高高度。
思路:
题目说明了独立求出每个颜色塔的最大可能的高度,因此我们只需要去找到一种颜色的求法,所有颜色的求法即可呼之欲出了。
我们对于第i个如果颜色是c[i],并且我们想要它上面第j个的颜色也是c[i],那么根据图示我们可以发现一个规律,就是i要走偶数步才能回到c[i]也就是说(j-i)%2==0
那么我们就可以得到了我们的决策,只要两个相同颜色之间的序号差是偶数,那么就可以从上一个转移过来。
如何表示这个过程呢?可以直接判定序号的奇偶性,偶数序号从偶数序号转移,奇数序号从奇数序号转移,最后取max即可
状态转移方程为:
f
a
i
,
i
&
1
=
m
a
x
(
f
a
i
,
i
&
1
,
f
a
i
,
!
(
i
&
1
)
)
f_{a_i,i\&1}=max(f_{a_i,i\&1},f_{a_i,!(i\&1)})
fai,i&1=max(fai,i&1,fai,!(i&1))
核心代码如下:
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
f[i][0]=f[i][1]=0;
for(int i=1;i<=n;i++)
f[a[i]][i&1]=max(f[a[i]][i&1],f[a[i]][!(i&1)]+1);
for(int i=1;i<=n;i++)
cout<<max(f[i][0],f[i][1])<<" ";
9.CF_1706C(1400)
点击此处直达
tag -> dp , greedy , implementation
标签:贪心,前缀和,dp
题目大意:
现在有一个初始的长度为n的一片楼房,其中每一栋楼房的高度为 h i h_i hi,现在定义第 i i i栋楼房,如果 h i − 1 < h i 并且 h i + 1 < h i h_{i-1}<h_i并且h_{i+1}<h_i hi−1<hi并且hi+1<hi那么就称第i栋楼房为酷楼房,现在你可以对任意一栋楼房去添加任意高度,问如何在添加最小高度的情况下,实现最多的酷楼房?(第一栋和最后一栋不可能为酷楼房)
思路:
本题是一个运用前缀和的贪心问题,如果说跟dp挂钩,也顶多是求前缀和的时候不是传统的 f i = f i − 1 + a i f_i=f_{i-1}+a_i fi=fi−1+ai,需要去考虑前缀和的转移求和方式。
我们根据题意可以明确得知,如果n是奇数,那么最多可以实现 ( n − 1 ) / 2 (n-1)/2 (n−1)/2个酷楼房,就只需要把偶数序号的楼房全部变成酷楼房然后计算所需要的代价即可
如果n为偶数,我们可以把剩下的 ( n − 2 ) (n-2) (n−2)个楼房划分为 ( n − 2 ) / 2 (n-2)/2 (n−2)/2组,出现三种情况:
1.每组都建设左面的一栋楼房
2.从第i组开始,前面的每一组都建设左面的楼房,后面的每一组都建设右面的楼房
3.每一组都建设右面的楼房
因此我们只需要对三种情况分别进行求解得出最小值即可
核心代码如下:
if(n%2==1)
{
ll ans=0;
for(int i=2;i<=n;i+=2)
{
if(a[i]>a[i-1]&&a[i]>a[i+1])
{
continue;
}
else
{
ans+=max(a[i-1],a[i+1])-a[i]+1;
}
}
cout<<ans<<endl;
continue;
}
for(int i=1;i<=n-2;i+=2)
{
f[(i+1)>>1]=f[((i+1)>>1)-1]+solve(i+1);
ff[(i+1)>>1]=ff[((i+1)>>1)-1]+solve(i+2);
}
ll ans1=ff[(n-2)/2];//全都是右
ll ans2=f[(n-2)/2];//全都是左
ll ans3=1e18;
for(int i=1;i<=(n-2)/2;i++)
{
ans3=min(ans3,f[i-1]+(ff[(n-2)/2]-ff[i-1]));
}
cout<<min(ans1,min(ans2,ans3))<<endl;
其中对每一栋楼所需要消耗的材料计算如下:
ll solve(int i)
{
if(a[i]>a[i-1]&&a[i]>a[i+1])
return 0;
else
return max(a[i+1],a[i-1])-a[i]+1;
}
10.CF_1692H(1700)*
点击此处直达
tag -> dp , greedy , math
标签:dp
题目大意:
Marian 在一个赌场,赌场的游戏规则如下:
每一轮开始前,玩家选一个在 1 1 1 到 1 0 9 10^9 109 。然后,掷出一个有着 1 0 9 10^9 109 面的骰子,会随机出现一个在 1 1 1 与 1 0 9 10^9 109 之间的数。如果玩家猜对了,他们的钱就会翻一番,否则他们的钱会被折半。
Marian 可以预测未来,他知道在接下来 n n n 轮里骰子上的数,即 x 1 , x 2 , . . . , x n x_1,x_2,...,x_n x1,x2,...,xn。
Marian 会选择三个整数 a , l a,l a,l 和 r r r( l ≤ r l \le r l≤r)。他会玩 l − r + 1 l-r+1 l−r+1 轮。每一轮,他都猜同一个数 a a a。一开始(在第 l l l 轮之前)他有 1 1 1 美元。
Marian 请你帮助他决定 a , l a,l a,l 和 r r r( 1 ≤ a ≤ 1 0 9 , 1 ≤ l ≤ r ≤ n 1\le a\le 10^9,1\le l\le r\le n 1≤a≤109,1≤l≤r≤n),让他最后的钱最多。
注:在折半或翻番的过程中不会进行游戏,也不会有精度问题。举个例子,Marian 在游戏中可能会有 1 1024 , 1 128 , 1 2 , 1 , 2 , 4 \frac{1}{1024},\frac{1}{128},\frac{1}{2},1,2,4 10241,1281,21,1,2,4 等等(任何可以表示为 2 t 2^t 2t 的数,其中 t t t 为非 0 0 0 整数)。
思路:
首先,题意可以转化为:
求一个区间 [ l , r ] [l,r] [l,r],一个数 a a a,使得 ∑ i = 1 r [ x i = a ] − ∑ i = 1 r [ x i ≠ a ] \sum _ { i = 1 } ^ { r } [ x _ { i } = a ] - \sum _ { i = 1 } ^ { r } [ x _ { i } \neq a ] ∑i=1r[xi=a]−∑i=1r[xi=a]最大。若有多组解输出任意一组。
那么首先考虑确定 a a a,那么把原序列等于 a a a的数改为 1,不等于 $a $的数改为 − 1 -1 −1,就变成了一个最大子段和问题,可以 $O(n) dp $求解。
如果枚举 a a a 呢?那么这题的复杂度就是 O ( n 2 ) O(n^2) O(n2),过不了。
怎么优化呢?在 dp 求解最大子段和的过程,我们可以把连续的$ -1$ 段合并为一个数,这样只用在出现 1 1 1 的时候进行 dp。对于下标 i,在所有 a a a必然只会存在一个 1 1 1,即$ a=x_i$ 的情况。所以我们只会 dp$ n $次。复杂度降至 O ( n ) O(n) O(n)。
设 $ls_i
表示使
表示使
表示使 k<i,x_k=x_i
的最大的
的最大的
的最大的 k$,则状态转移方程为:
f
i
=
m
a
x
(
f
i
s
i
−
(
i
−
l
s
i
−
1
)
,
0
)
+
1
f _ { i } = m a x ( f _ { i s i } - ( i - l s _ { i } - 1 ) , 0 ) + 1
fi=max(fisi−(i−lsi−1),0)+1
输出方案的话可以记录下
f
i
f_i
fi 是从哪个地方转移过来的。具体可以参考代码。
求解 ls_i也很简单,在读入时用 map
预处理一下即可,复杂度
O
(
n
l
o
g
n
)
O(nlog n)
O(nlogn)。所以总复杂度也为
O
(
n
l
o
g
n
)
O(nlog n)
O(nlogn)
代码如下:
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
#include<map>
#include<cmath>
using namespace std;
typedef long long ll;
const int N = 2e5 + 10;
int t, n, x[N], ls[N], f[N], l[N];
int main(){
scanf("%d", &t);
while(t--){
scanf("%d", &n);
map<int, int> mp;
for(int i = 1; i <= n; ++ i){
scanf("%d", &x[i]);
ls[i] = mp[x[i]];
mp[x[i]] = i;
}
int ans = 0, pos;
for(int i = 1; i <= n; ++ i){
if(f[ls[i]] - (i-ls[i]-1) > 0){
f[i] = f[ls[i]] - (i-ls[i]-1) + 1;
l[i] = l[ls[i]];
} else {
f[i] = 1;
l[i] = i;
}
if(f[i] > ans){
ans = f[i]; pos = i;
}
}
printf("%d %d %d\n", x[pos], l[pos], pos);
}
}