今天的算法是动态规划算法部分,不求多,求理解和复习的全面
学习1:动态规划题单刷题:
普通线性规划:n^2
租用游艇
题目描述
长江游艇俱乐部在长江上设置了 n 个游艇出租站 1,2,⋯,n。游客可在这些游艇出租站租用游艇,并在下游的任何一个游艇出租站归还游艇。游艇出租站 i 到游艇出租站 j 之间的租金为 r(i,j)(1≤i<j≤n)。试设计一个算法,计算出从游艇出租站 1 到游艇出租站 n 所需的最少租金。
输入格式
第一行中有一个正整数 n,表示有 n 个游艇出租站。接下来的 n−1 行是一个半矩阵 r(i,j)(1≤i<j≤n)。
输出格式
输出计算出的从游艇出租站 1 到游艇出租站 n 所需的最少租金。
遍历所有可能转移过来的可能即可:
#include <bits/stdc++.h>
using namespace std;
const int N=1010;
int n;
int r[N][N];
int f[N];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=i+1;j<=n;j++){
scanf("%d",&r[i][j]);
r[j][i]=r[i][j];
}
}
memset(f,0x3f,sizeof f);
f[1]=0;
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
f[i]=min(f[i],f[j]+r[i][j]);
}
}
cout<< f[n]<<endl;
}
普通0-1背包问题:
[NOIP 2006 普及组] 开心的金明
题目描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N 元钱就行”。今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的 N 元。于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1−5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过 N 元(可以等于 N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第j件物品的价格为 vj,重要度为 wj,共选中了 k 件物品,编号依次为 j1,j2,…,jk,则所求的总和为:
vj1×wj1+vj2×wj2…+vjk×wjk
请你帮助金明设计一个满足要求的购物单。
输入格式
第一行,为 2 个正整数,用一个空格隔开:n,m(n<30000,m<25)其中 n 表示总钱数,m 为希望购买物品的个数。
从第 2 行到第 m+1 行,第 j 行给出了编号为 j−1 的物品的基本数据,每行有 2 个非负整数 v,p(其中 v 表示该物品的价格 (v≤10000),p 表示该物品的重要度(1≤p≤5)。
输出格式
1 个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<100000000)。
模板题目:
#include<iostream>
#include<algorithm>
using namespace std;
int w[30],v[30],f[50000];
int n,m;
int main()
{
cin>>m>>n;
for(int i=1;i<=n;i++)
{
cin>>v[i]>>w[i];
w[i]*=v[i];
}
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)//滚动数组后,从后向前遍历
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}
特殊转移的背包问题:
5 倍经验日
题目背景
现在乐斗有活动了!每打一个人可以获得 5 倍经验!absi2011 却无奈的看着那一些比他等级高的好友,想着能否把他们干掉。干掉能拿不少经验的。
题目描述
现在 absi2011 拿出了 x 个迷你装药物(嗑药打人可耻…),准备开始与那些人打了。
由于迷你装药物每个只能用一次,所以 absi2011 要谨慎的使用这些药。悲剧的是,用药量没达到最少打败该人所需的属性药药量,则打这个人必输。例如他用 2 个药去打别人,别人却表明 3 个药才能打过,那么相当于你输了并且这两个属性药浪费了。
现在有 n 个好友,给定失败时可获得的经验、胜利时可获得的经验,打败他至少需要的药量。
要求求出最大经验 s,输出 5s。
输入格式
第一行两个数,n 和 x。
后面 n 行每行三个数,分别表示失败时获得的经验 losei,胜利时获得的经验 wini 和打过要至少使用的药数量 usei。
输出格式
一个整数,最多获得的经验的五倍。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100100;
int n,m;
int l[N],w[N],c[N];
int f[N];
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>l[i]>>w[i]>>c[i];
}
for(int i=1;i<=n;i++){
for(int j=m;j>=c[i];j--){
f[j]=max(f[j-c[i]]+w[i],f[j]+l[i]);//输了or赢了
}
for(int j=c[i]-1;j>=0;j--)
f[j]=f[j]+l[i];//输了也有经验值,而且输的话可以不花费药水
}
int res=max((long long)0,5*f[m]);
cout<<res;
}
装箱问题
题目描述
有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积。
现在从 n 个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。
输入格式
第一行共一个整数 V,表示箱子容量。
第二行共一个整数 n,表示物品总数。
接下来 n 行,每行有一个正整数,表示第 i 个物品的体积。
输出格式
共一行一个整数,表示箱子最小剩余空间。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100100;
int n,m;
int c[N];
int f[N];
signed main(){
cin>>m>>n;
for(int i=1;i<=n;i++){
cin>>c[i];
}
memset(f,-0x3f,sizeof f);
f[0]=0;
for(int i=1;i<=n;i++){
for(int j=m;j>=c[i];j--)
f[j]=max(f[j],f[j-c[i]]+1);
}
int res=m+1;
for(int i=m;i;i--){
if(f[i]>=0&&res==m+1){
res=i;
break;
}
}
cout<<m-res;
}
分组背包:
通天之分组背包
题目背景
直达通天路·小 A 历险记第二篇
题目描述
自 01 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 01 背包,他的物品大致可分为 k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
输入格式
两个数 m,n,表示一共有 n 件物品,总重量为 m。
接下来 n 行,每行 3 个数 ai,bi,ci,表示物品的重量,利用价值,所属组数。
输出格式
一个数,最大的利用价值。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=10010;
int f[N];
int g[N];
int w[N],v[N];
unordered_map<int,vector<int>> e;
int n,m;
int ids=INT_MAX,ide=INT_MIN;
int main(){
cin>>m>>n;
for(int i=1;i<=n;i++){
int id;
cin>>w[i]>>v[i]>>id;
e[id].push_back(i);
ids=min(id,ids);
ide=max(ide,id);
}
for(int i=ids;i<=ide;i++){
memcpy(g,f,sizeof f);
for(int p:e[i]){
for(int j=m;j>=w[p];j--){
f[j]=max(f[j],g[j-w[p]]+v[p]);
}
}
}
cout<<f[m];
}
完全背包
A+B Problem(再升级)(复习**)
题目背景
题目名称是吸引你点进来的。
实际上该题还是很水的。
题目描述
1+1=? 显然是 2。
a+b=? P1001 回看不谢。
哥德巴赫猜想 似乎已呈泛滥趋势。
以上纯属个人吐槽
给定一个正整数 n,求将其分解成若干个素数之和的方案总数。
输入格式
一行一个正整数 n。
输出格式
一行一个整数表示方案总数。
代码:
#include <iostream>
#include <cstring>
#define int long long
using namespace std;
const int N = 1010;
int pn = 0; // 素数的个数
int su[N]; // 存储素数
int f[N]; // f[i] 表示将 i 分解为素数之和的方案总数
bool st[N]; // 标记数组,用于筛选素数
// 筛选素数
void get_p(int n) {
memset(st, false, sizeof st);
for (int i = 2; i <= n; i++) {
if (!st[i]) {
su[++pn] = i;
}
for (int j = 1; j <= pn && su[j] * i <= n; j++) {
st[su[j] * i] = true;
if (i % su[j] == 0) break;
}
}
}
signed main(){
int n;
cin >> n;
get_p(n);
memset(f, 0, sizeof f);
f[0] = 1; // 组成 0 的方案数为 1
for (int i = 1; i <= pn; i++)
for(int j=su[i];j<=n;j++)
f[j]+=f[j-su[i]];
cout << f[n] << endl;
return 0;
}
二维0-1背包:
NASA的食物计划
题目背景
NASA(美国航空航天局)因为航天飞机的隔热瓦等其他安全技术问题一直大伤脑筋,因此在各方压力下终止了航天飞机的历史,但是此类事情会不会在以后发生,谁也无法保证。所以,在遇到这类航天问题时,也许只能让航天员出仓维修。但是过多的维修会消耗航天员大量的能量,因此 NASA 便想设计一种食品方案,使体积和承重有限的条件下多装载一些高卡路里的食物。
题目描述
航天飞机的体积有限,当然如果载过重的物品,燃料会浪费很多钱,每件食品都有各自的体积、质量以及所含卡路里。在告诉你体积和质量的最大值的情况下,请输出能达到的食品方案所含卡路里的最大值,当然每个食品只能使用一次。
输入格式
第一行 2 个整数,分别代表体积最大值 h 和质量最大值 t。
第二行 1 个整数代表食品总数 n。
接下来 n 行每行 3 个数 体积 hi,质量 ti,所含卡路里 ki。
输出格式
一个数,表示所能达到的最大卡路里(int 范围内)
代码;
#include<iostream>
using namespace std;
int a[51],b[51],c[51];
int f[501][501];
int main()
{
int n,m,v;
cin>>m>>v>>n;
for(int i=1;i<=n;i++)
cin>>a[i]>>b[i]>>c[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=a[i];j--)
for(int k=v;k>=b[i];k--)
f[j][k]=max(f[j][k],f[j-a[i]][k-b[i]]+c[i]);
cout<<f[m][v];
return 0;
}
区间dp:
约翰经常给产奶量高的奶牛发特殊津贴,于是很快奶牛们拥有了大笔不知该怎么花的钱。为此,约翰购置了 N(1≤N≤2000) 份美味的零食来卖给奶牛们。每天约翰售出一份零食。当然约翰希望这些零食全部售出后能得到最大的收益,这些零食有以下这些有趣的特性:
零食按照 1,…,N 编号,它们被排成一列放在一个很长的盒子里。盒子的两端都有开口,约翰每天可以从盒子的任一端取出最外面的一个。
与美酒与好吃的奶酪相似,这些零食储存得越久就越好吃。当然,这样约翰就可以把它们卖出更高的价钱。
每份零食的初始价值不一定相同。约翰进货时,第i份零食的初始价值为 Vi(1≤V≤1000)。
第i份零食如果在被买进后的第 a 天出售,则它的售价是 Vi×a。
Vi 的是从盒子顶端往下的第i份零食的初始价值。约翰告诉了你所有零食的初始价值,并希望你能帮他计算一下,在这些零食全被卖出后,他最多能得到多少钱。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,f[2017][2017],v[2017];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&v[i]);
for(int i=1;i<=n;i++)f[i][i]=v[i]*n;
for(int i=2;i<=n;i++){//长度,同时也表示倒数第几个卖当前的抉择
for(int l=1;l<=n;l++){//左端点
int r=l+i-1;
if(r>n)break;
f[l][r]=max(f[l][r-1]+v[r]*(n-i+1),f[l+1][r]+v[l]*(n-i+1));//卖右边or卖左边
}
}
printf("%d\n",f[1][n]);
return 0;
}
状态机dp:
NOIP2007 普及组 T3
题目描述
恶魔猎手尤迪安野心勃勃,他背叛了暗夜精灵,率领深藏在海底的娜迦族企图叛变。
守望者在与尤迪安的交锋中遭遇了围杀,被困在一个荒芜的大岛上。
为了杀死守望者,尤迪安开始对这个荒岛施咒,这座岛很快就会沉下去。到那时,岛上的所有人都会遇难。
守望者的跑步速度为 17m/s,以这样的速度是无法逃离荒岛的。庆幸的是守望者拥有闪烁法术,可在 1s 内移动 60m,不过每次使用闪烁法术都会消耗魔法值 10 点。守望者的魔法值恢复的速度为 4 点每秒,只有处在原地休息状态时才能恢复。
现在已知守望者的魔法初值 M,他所在的初始位置与岛的出口之间的距离 S,岛沉没的时间 T。你的任务是写一个程序帮助守望者计算如何在最短的时间内逃离荒岛,若不能逃出,则输出守望者在剩下的时间内能走的最远距离。
注意:守望者跑步、闪烁或休息活动均以秒为单位,且每次活动的持续时间为整数秒。距离的单位为米。
输入格式
输入数据共一行三个非负整数,分别表示 M,S,T。
输出格式
输出数据共两行。
第一行一个字符串 Yes 或 No,即守望者是否能逃离荒岛。
第二行包含一个整数。第一行为 Yes 时表示守望者逃离荒岛的最短时间;第一行为 No 时表示守望者能走的最远距离。
代码:
#include<bits/stdc++.h>
using namespace std;
int m,s,t,fla,run;
//两个状态,一个能闪就闪,不能就原地休息,另外一个只知道跑
int main(){
cin>>m>>s>>t;
for(int i=1;i<=t;i++){
if(m>=10)m-=10,fla+=60,run+=17;//能够闪现
else{//不能闪现
if(fla>run)run=fla;//聪明的跑者知道复制另外一个人的选择
m+=4,run+=17;//回复魔法+跑步
}
if(max(fla,run)>=s){
printf("Yes\n%d\n",i);return 0;
}
}
cout<<"No"<<endl<<max(fla,run)<<endl;
return 0;
}
[NOIP 2012 普及组] 摆花
题目描述
小明的花店新开张,为了吸引顾客,他想在花店的门口摆上一排花,共 m 盆。通过调查顾客的喜好,小明列出了顾客最喜欢的 n 种花,从 1 到 n 标号。为了在门口展出更多种花,规定第 i 种花不能超过 ai 盆,摆花时同一种花放在一起,且不同种类的花需按标号的从小到大的顺序依次摆列。
试编程计算,一共有多少种不同的摆花方案。
输入格式
第一行包含两个正整数 n 和 m,中间用一个空格隔开。
第二行有 n 个整数,每两个整数之间用一个空格隔开,依次表示 a1,a2,⋯,an。
输出格式
一个整数,表示有多少种方案。注意:因为方案数可能很多,请输出方案数对 106+7 取模的结果。
代码;
#include <bits/stdc++.h>
using namespace std;
const int MOD=1e6+7;
const int N=110;
int f[N]={0};
int a[N]={0};
int main(){
int n,m;
cin>>n>>m;
f[0]=1;
int pre=0;
for(int i=1;i<=n;i++)scanf("%d" ,&a[i]);
for(int i=1;i<=n;i++){//考虑第i种花
pre+=a[i];//前缀和,表示当前最多能种的花的数量
for(int j=min(pre,m);j>=0;j--){//体积
for(int k=1;k<=min(j,a[i]);k++){//考虑第i种花种的个数
f[j]=(f[j]+f[j-k])%MOD;
}
}
}
cout<<f[m];
}
[NOIP 2004 提高组] 合唱队形
题目描述
n 位同学站成一排,音乐老师要请其中的 n−k 位同学出列,使得剩下的 k 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 k 位同学从左到右依次编号为 1,2, … ,k,他们的身高分别为 t1,t2, … ,tk,则他们的身高满足 t1<⋯<ti>ti+1> … >tk(1≤i≤k)。
你的任务是,已知所有 n 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入格式
共二行。
第一行是一个整数 n(2≤n≤100),表示同学的总数。
第二行有 n 个整数,用空格分隔,第 i 个整数 ti(130≤ti≤230)是第 i 位同学的身高(厘米)。
输出格式
一个整数,最少需要几位同学出列。
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int a[N];
int f1[N],f2[N];
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
if(a[i]>a[j])
f1[i]=max(f1[i],f1[j]+1);
}
}
for(int i=n;i;i--){
for(int j=n;j>i;j--){
if(a[i]>a[j])
f2[i]=max(f2[i],f2[j]+1);
}
}
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,f1[i]+f2[i]);
}
cout<<n-ans-1;
}
区间dp:
[IOI 2000] 回文字串
题目描述
回文词是一种对称的字符串。任意给定一个字符串,通过插入若干字符,都可以变成回文词。此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数。
比如 Ab3bd 插入 2 个字符后可以变成回文词 dAb3bAd 或 Adb3bdA,但是插入少于 2 个的字符无法变成回文词。
注意:此问题区分大小写。
输入格式
输入共一行,一个字符串。
输出格式
有且只有一个整数,即最少插入字符数。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N][N];
int main(){
string s;
cin>>s;
int n=s.size();
for(int i=2;i<=n;i++){
for(int l=0;l+i-1<n;l++){
int r=l+i-1;
if(s[l]==s[r])f[l][r]=f[l+1][r-1];
else f[l][r]=min(f[l][r-1]+1,f[l+1][r]+1);
}
}
cout<<f[0][n-1];
}
[CSP-J2019] 纪念品(复习***)
题目描述
小伟突然获得一种超能力,他知道未来 T 天 N 种纪念品每天的价格。某个纪念品的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。
每天,小伟可以进行以下两种交易无限次:
任选一个纪念品,若手上有足够金币,以当日价格购买该纪念品;
卖出持有的任意一个纪念品,以当日价格换回金币。
每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。当然,一直持有纪念品也是可以的。
T 天之后,小伟的超能力消失。因此他一定会在第 T 天卖出所有纪念品换回金币。
小伟现在有 M 枚金币,他想要在超能力消失后拥有尽可能多的金币。
输入格式
第一行包含三个正整数 T,N,M,相邻两数之间以一个空格分开,分别代表未来天数 T,纪念品数量 N,小伟现在拥有的金币数量 M。
接下来 T 行,每行包含 N 个正整数,相邻两数之间以一个空格分隔。第 i 行的 N 个正整数分别为 Pi,1,Pi,2,…,Pi,N,其中 Pi,j 表示第 i 天第 j 种纪念品的价格。
输出格式
输出仅一行,包含一个正整数,表示小伟在超能力消失后最多能拥有的金币数量。
代码:
#include <iostream>
#include <memory.h>
using namespace std;
const int N = 101;
const int M = 10001;
int n, m, t, price[N][N], f[M];
int main()
{
cin >> t >> n >> m;
for(int i = 1; i <= t; i++)
for(int j = 1; j <= n; j++)
cin >> price[j][i];
//
for(int k = 1; k < t; k++)
{
memset(f, 0, sizeof f);
for(int i = 1; i <= n; i++)
for(int j = price[i][k]; j <= m; j++)
f[j] = max(f[j], f[j - price[i][k]] + price[i][k + 1] - price[i][k]);//今天买,明天卖
m += f[m];//加上盈利的钱,进入第二天
}
cout << m;
return 0;
}