题目来源:https://www.cnblogs.com/henry-1202/p/9211398.html#_label0
蒟蒻照搬的题目orz 各位要学习的话建议click ↑↑
文章目录
1.合唱队形
传送门
队形长度即为:
最长上升子序列+最长下降子序列 -1
#include<bits/stdc++.h>
using namespace std;
int n,up[110],down[110];
int h[110];
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
cin>>h[i];
for(int i=1;i<=n;++i)
up[i]=1,down[i]=1;
for(int i=n-1;i>=1;--i)
for(int j=i+1;j<=n;++j)
if(h[i]>h[j]) down[i]=max(down[i],down[j]+1);
for(int i=2;i<=n;++i)
for(int j=i-1;j>=1;--j)
if(h[j]<h[i]) up[i]=max(up[i],up[j]+1);
int ans=0;
for(int i=1;i<=n;++i)
ans=max(ans,down[i]+up[i]-1);
cout<<n-ans;
}
2.导弹拦截
传送门
系统最多拦截弹导数:最长不下降子序列
最少拦截所有导弹系统数:最长上升子序列
n^2做法(传统做法)
#include<bits/stdc++.h>
using namespace std;
int h[110000],down[110000],up[110000];
int main()
{
int n=0;
for(int i=1;scanf("%d",&h[i])!=EOF;++i)
down[i]=1,up[i]=1,++n;
for(int i=2;i<=n;++i)
for(int j=i-1;j>=1;--j)
{
if(h[j]>=h[i]) down[i]=max(down[i],down[j]+1);
if(h[j]<h[i]) up[i]=max(up[i],up[j]+1);
}
int num=0,cnt=0;
for(int i=1;i<=n;++i)
num=max(num,down[i]), //这套系统最多拦截导弹数
cnt=max(cnt,up[i]); //最少拦截导弹系统数
cout<<num<<endl<<cnt;
}
n
l
o
g
n
nlog_n
nlogn做法(STL函数 二分更快 )这种算法只能求长度,无法输出子序列
后面写最长公共子序列有写 二分模板
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N], d1[N], d2[N], n;
int main()
{
while (cin>>a[++n]);
n--; //输入
int len1=1, len2= 1; //初始长度为1
d1[1]=a[1]; //用于求不上升序列长度
d2[1]=a[1]; //用于求上升序列长度
for(int i=2; i<=n; i++)
{ //从a[2]开始枚举每个数(a[1]已经加进去了)
if(d1[len1]>=a[i]) d1[++len1]=a[i]; //如果满足要求(不上升)就加入d1
else
{ //否则用a[i]替换d1中的一个数
int p1=upper_bound(d1+1, d1+1+len1,a[i],greater<int>())-d1;
d1[p1]=a[i];
}
if (d2[len2]<a[i]) d2[++len2]=a[i]; //同上
else
{
int p2=lower_bound(d2+1, d2+1+len2,a[i])-d2;
d2[p2]=a[i];
}
}
cout<<len1<<endl<<len2; //输出
return 0; //结束
}
3.尼克的任务
传送门
显然动态转移方程:
(
本
时
刻
无
任
务
)
f
[
i
]
=
f
[
i
+
1
]
+
1
(
本
时
刻
有
任
务
)
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
i
+
a
[
s
u
m
]
)
(本时刻无任务)f[i]=f[i+1]+1\\ (本时刻有任务)f[i]=max(f[i],f[i+a[sum])
(本时刻无任务)f[i]=f[i+1]+1(本时刻有任务)f[i]=max(f[i],f[i+a[sum])
可见状态f[i]与后面的时刻有关
⇒
\Rightarrow
⇒倒着递推
*在处理方面:先将任务排序,用从大到小用任务个数枚举
#include<bits/stdc++.h>
using namespace std;
const int N=11000;
int n,k,p[N],t[N],cnt[N],f[N];
struct task
{
int p,t;
} z[N];
bool cmp(task x,task y)
{
return x.p<y.p;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=k;++i)
{
scanf("%d%d",&z[i].p,&z[i].t);
++cnt[z[i].p];
}
sort(z+1,z+1+k,cmp);
int num=k;
for(int i=n;i>=1;--i)
{
if(!cnt[i]) f[i]=f[i+1]+1;
else
for(int j=1;j<=cnt[i];++j)
{
f[i]=max(f[i],f[i+z[num].t]);
num--;
}
}
cout<<f[1];
}
4.丝绸之路
传送门
f[i][j] 表示到第i个城市时为第j天
由题意可知 有两种状态:
移
动
:
f
[
i
−
1
]
[
j
−
1
]
+
d
[
i
]
∗
c
[
j
]
(
昨
天
在
i
−
1
城
市
,
今
天
走
到
i
城
市
)
休
息
:
f
[
i
]
[
j
−
1
]
f
[
i
−
1
]
[
j
−
1
]
+
d
[
i
]
∗
c
[
j
]
(
昨
天
就
到
了
i
城
市
,
在
i
城
市
休
息
了
一
天
)
转
移
方
程
:
f
[
i
]
[
j
]
=
m
i
n
(
移
动
,
休
息
)
移动:f[i-1][j-1]+d[i]*c[j](昨天在i-1城市,今天走到i城市)\\ 休息:f[i][j-1]f[i-1][j-1]+d[i]*c[j](昨天就到了i城市,在i城市休息了一天)\\ 转移方程:f[i][j]=min(移动,休息)
移动:f[i−1][j−1]+d[i]∗c[j](昨天在i−1城市,今天走到i城市)休息:f[i][j−1]f[i−1][j−1]+d[i]∗c[j](昨天就到了i城市,在i城市休息了一天)转移方程:f[i][j]=min(移动,休息)
#include<bits/stdc++.h>
using namespace std;
const int N=1100,M=1100;
int n,m,f[N][M],d[N],c[M];
void dp()
{
memset(f,0x7f,sizeof(f)); //初始化最大值
for(int i=0;i<=m;i++) f[0][i]=0; //初始化边界值
for(int i=1;i<=n;++i)
for(int j=i;j<=m-n+i;++j)
{
int rest=f[i][j-1], //昨天就到了i城市,在i城市休息了一天
move=f[i-1][j-1]+d[i]*c[j]; //昨天在i-1城市,今天走到i城市
f[i][j]=min(rest,move);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&d[i]);
for(int i=1;i<=m;++i)
scanf("%d",&c[i]);
dp();
cout<<f[n][m];
}
5.分队问题
传送门
u1s1第一眼确实没想到dp
用
f
[
i
]
f[i]
f[i]表示有i个人时可满足的最多队伍数
(在dp循环中 i也表示此时在给第n个人编队)
要把容易满足的放在最前面编队所以要先排序(不会影响结果)
那么对于i有两种状态:
i
<
a
[
i
]
时
f
[
i
]
=
f
[
i
−
1
]
i<a[i]时 \ \ \ f[i]=f[i-1]
i<a[i]时 f[i]=f[i−1]第i个人没有选择只能直接加入之前的队列
i
≥
a
[
i
]
时
f
[
i
]
=
m
a
x
(
f
[
i
−
1
]
,
f
[
i
−
a
[
i
]
+
1
)
i\ge a[i]时\ \ \ f[i]=max(f[i-1],f[i-a[i]+1)
i≥a[i]时 f[i]=max(f[i−1],f[i−a[i]+1)第i个人的要求已经可以满足了,他可以拉a[i]个人出去单独编一队,也可以继续加入原队
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,a[N],f[N];
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
scanf("%d",&a[i]);
sort(a+1,a+1+n); //从小到大排序 要求数目没变不影响结果
for(int i=1;i<=n;++i)
{
if(a[i]>i)
f[i]=f[i-1];
else //a[i]<=i此时满足i最少人数要求了
f[i]=max(f[i-a[i]]+1,f[i-1]);
//i拉a[i]个人单独组队 或 直接加入原来的队
}
cout<<f[n];
}
6.低价购买
传送门
题目有两个问题:最大购买次数 和 最大购买次数的方案数
第一个问题 最长下降子序列 上面全是模板(用
d
o
w
n
[
i
]
down[i]
down[i]表示)
第二个问题 实际上也是一种dp
sum[i]表示以i节点为头,长度为
d
o
w
n
[
i
]
down[i]
down[i]的子序列的个数(最终答案就是累加
d
o
w
n
[
i
]
down[i]
down[i]为最大值时的
s
u
m
[
i
]
sum[i]
sum[i])
s
u
m
[
i
]
sum[i]
sum[i]为所有 接在i后面的序列的$sum[] $的总值
a
[
i
]
>
a
[
j
]
且
d
o
w
n
[
i
]
=
d
o
w
n
[
i
]
+
1
s
u
m
[
i
]
+
=
s
u
m
[
j
]
a[i]>a[j]且 down[i]=down[i]+1 \ \ \ \ sum[i]+=sum[j]
a[i]>a[j]且down[i]=down[i]+1 sum[i]+=sum[j]
题意还要求去除重复的序列,由于我们在求LIS时只保存了序列的长度和开头,则考虑开头相同且长度也相同的序列去重
a
[
i
]
=
a
[
j
]
且
d
o
w
n
[
i
]
=
d
o
w
n
[
i
]
s
u
m
[
i
]
=
0
a[i]=a[j]且 down[i]=down[i]\ \ \ \ sum[i]=0
a[i]=a[j]且down[i]=down[i] sum[i]=0
不知道为什么把 求sum 和 去重 分开写是错的只好合在一起
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+10;
long long n,a[N],down[N],sum[N];
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)
down[i]=1; //初始化down[]边界值
for(int i=n;i>=1;--i)
for(int j=i+1;j<=n;++j)
if(a[i]>a[j])
down[i]=max(down[i],down[j]+1);
//求最长下降子序列
for(int i=1;i<=n;++i)
if(down[i]==1) sum[i]=1; //初始化sum[]边界值
for(int i=n;i>=1;--i)
for(int j=i+1;j<=n;++j)
if(a[i]>a[j]&&down[i]==down[j]+1) sum[i]+=sum[j];
else if(a[i]==a[j]&&down[i]==down[j]) sum[j]=0;
//求以i节点为头,长度为down[i]的子序列 的个数
//除去重复的序列
long long m=0,ans=0;
for(int i=1;i<=n;++i)
m=max(m,down[i]);
for(int i=1;i<=n;++i)
if(down[i]==m) ans+=sum[i];
cout<<m<<" "<<ans;
}
7.回文子串
传送门
因为回文的性质,我们想到把原串先倒序复制一份,那么两个字符串的最长公共子序列就是原串已经满足回文的部分
需要添加的长度即 原串长度-最长公共子序列的长度
问题转化为求最长公共子序列:
i
f
(
c
h
[
i
]
=
c
h
[
j
]
)
f
[
i
]
[
j
]
=
f
[
i
−
1
]
[
j
−
1
]
+
1
e
l
s
e
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
−
1
]
[
j
]
,
f
[
i
]
[
j
−
1
]
)
if(ch[i]=ch[j])\ \ \ f[i][j]=f[i-1][j-1]+1\\ else\ \ \ f[i][j]=max(f[i-1][j],f[i][j-1])
if(ch[i]=ch[j]) f[i][j]=f[i−1][j−1]+1else f[i][j]=max(f[i−1][j],f[i][j−1])
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int f[N][N];
char s1[N],s2[N];
int main()
{
gets(s1+1); int n=strlen(s1+1);
for(int i=1;i<=n;++i)
s2[i]=s1[n-i+1]; //倒序复制s1到s2
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
if(s1[i]==s2[j]) f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i-1][j],f[i][j-1]);
cout<<n-f[n][n];
}
但是对于二维数组空间复杂度着实吓人
我们观察到
f
[
i
]
[
j
]
f[i][j]
f[i][j] 数组的第一维只在
f
[
i
−
1
]
[
j
]
f[i-1][j]
f[i−1][j] 和
f
[
i
−
1
]
[
j
−
1
]
f[i-1][j-1]
f[i−1][j−1] 中用到了
所以第一维可以用一个滚动数组
f
2
f_2
f2代替 其实我也不知道这是啥
即
f
2
[
]
=
f
[
i
−
1
]
[
]
f_2[]=f[i-1][]
f2[]=f[i−1][]
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int f1[N],f2[N]; //f2[]就相当于f[i-1][]
char s1[N],s2[N];
int main()
{
gets(s1+1); int n=strlen(s1+1);
for(int i=1;i<=n;++i)
s2[i]=s1[n-i+1]; //倒序复制s1到s2
for(int i=1;i<=n;++i)
{
for(int j=1;j<=n;++j)
if(s1[i]==s2[j]) f1[j]=f2[j-1]+1; //f[i-1][j-1]变为f2[j-1]
else f1[j]=max(f2[j],f1[j-1]); //f[i-1][j]变为f2[j]
memcpy(f2,f1,sizeof(f1));
}
cout<<n-f1[n];
}
然后下一道题就是最长公共子序列的 n l o g n nlog_n nlogn版
8.最长公共子序列
传送门
最长公共子序列的
n
l
o
g
n
nlog_n
nlogn版实际上是从最长上升子序列转换过来的
我们把数字的大小按照第一个序列从左到右 是 从小到大 的规则
求出第二个序列在此规则下的上升子序列就是公共子序列了
(第二个序列中若有元素不属于第一个序列直接不管就是,反正肯定不是公共子序列)
上升子序列 我写过STL函数的写法 这里就手写二分
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],b[N],m[N];
int sstack[N],top; //手写栈
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&a[i]),m[a[i]]=i; //m数组用以对应a[i]的序号
for(int i=1;i<=n;++i)
scanf("%d",&b[i]);
for(int i=1;i<=n;++i)
b[i]=m[b[i]]; //用m[]对应转化第二个序列
for(int i=1;i<=n;++i)
{
if(b[i]>sstack[top]) sstack[++top]=b[i];
else
{
int l=1,r=top;
while(l<r)
{
int mid=(l+r)>>1;
if(b[i]>sstack[mid]) l=mid+1;
else r=mid;
}
sstack[l]=b[i];
}
}
cout<<top;
}
9.魔族秘密
传送门
如果把a是b的子串转化为a<b,这道题就变成了求最长上升子序列…
转化方法:hash,string(函数),char(strstr函数)…
我这里写的是strstr函数
这不应该是一道字典树题嘛…
#include<bits/stdc++.h>
using namespace std;
const int N=2e3+10,M=80;
int n,f[N];
char s[N][M];
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
scanf("%s",s[i]);
for(int i=1;i<=n;++i)
f[i]=1; //差点又忘了初始化
for(int i=n;i>=1;--i)
for(int j=i+1;j<=n;++j)
if(strstr(s[j],s[i])==s[j]) f[i]=max(f[i],f[j]+1);
//即表示i是j的子串
int ans=0;
for(int i=1;i<=n;++i)
ans=max(ans,f[i]);
cout<<ans;
}
10.创意吃鱼法
传送门
有点毒瘤
用
f
1
f
2
f_1\ f_2
f1 f2分别维护向右斜和向左斜,以
(
i
,
j
)
(i,j)
(i,j)为终止的连续的1的个数
则可以两层循环dp,对每一个满足
a
[
i
]
[
j
]
=
1
a[i][j]=1
a[i][j]=1的点,有:
f
1
[
i
]
[
j
]
=
m
i
n
(
f
1
[
i
−
1
]
[
j
−
1
]
,
m
i
n
(
c
n
t
1
[
i
]
[
j
−
1
]
,
c
n
t
2
[
i
−
1
]
[
j
]
)
)
+
1
f
2
[
i
]
[
j
]
=
m
i
n
(
f
2
[
i
−
1
]
[
j
+
1
]
,
m
i
n
(
c
n
t
2
[
i
−
1
]
[
j
]
,
c
n
t
3
[
i
]
[
j
+
1
]
)
)
+
1
,
f1[i][j]=min(f1[i-1][j-1],min(cnt1[i][j-1],cnt2[i-1][j]))+1\\ f2[i][j]=min(f2[i-1][j+1],min(cnt2[i-1] [j],cnt3[i][j+1]))+1,
f1[i][j]=min(f1[i−1][j−1],min(cnt1[i][j−1],cnt2[i−1][j]))+1f2[i][j]=min(f2[i−1][j+1],min(cnt2[i−1][j],cnt3[i][j+1]))+1,
那么很关键的
c
n
t
1
2
3
[
i
]
[
j
]
cnt1\ 2\ 3[i][j]
cnt1 2 3[i][j]则是需要预处理的数组用以判断在递推时最多能够连上几个1
(题上说正方形对角线之外的数字必须是0 不会只有我没看到吧)
c
n
t
1
2
3
cnt1\ 2\ 3
cnt1 2 3分别表示从
(
i
,
j
)
(i,j)
(i,j)向左 上 右 连续0的个数
还有一根问题这么多N*M的数组空间复杂度很危险 于是在这里引入
s
h
o
r
t
short
short 前提是数字都不超过32767变量
#include<bits/stdc++.h>
using namespace std;
const int N=2600,M=2600;
short n,m,a[N][M],ans;
short f1[N][M],f2[N][M];
//f1表示(i,j)为末,向右向下连续1个数
//f2表示(i,j)为末,向左向下连续1个数
short cnt1[N][M],cnt2[N][M],cnt3[N][M];
//cnt1表示向左数 连续0的个数
//cnt2表示向上数 连续0的个数
//cnt3表示向右数 连续0的个数
short read() //读入优化
{
short x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f; //我每次写读入优化都要忘这个
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
{
a[i][j]=read();
if(!a[i][j])
cnt1[i][j]=cnt1[i][j-1]+1,
cnt2[i][j]=cnt2[i-1][j]+1; //预处理左 上方向0的个数
}
for(int i=1;i<=n;++i)
for(int j=m;j>=1;--j)
if(!a[i][j])
cnt3[i][j]=cnt3[i][j+1]+1; //预处理右方向0的个数
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if(a[i][j])
{
f1[i][j]=min(f1[i-1][j-1],min(cnt1[i][j-1],cnt2[i-1][j]))+1,
f2[i][j]=min(f2[i-1][j+1],min(cnt2[i-1][j],cnt3[i][j+1]))+1,
//同时满足三者条件所以用min
ans=max(max(f1[i][j],f2[i][j]),ans);
}
cout<<ans;
}
11.王子和公主
(Prince and Princess)
传送门(luogu)
传送门(VJ)
这题就是求最长公共子序列,详情请见8题模板
(交到VJ上 不明原因T了望大佬康康)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int nn,n,m,a[N],b[N],M[N];
int sstack[N],top; //手写栈
int main()
{
int t;
scanf("%d",&t);
for(int k=1;k<=t;++k)
{
memset(M,0,sizeof(M));
top=0;
scanf("%d%d%d",&nn,&n,&m);
for(int i=1;i<=n+1;++i)
scanf("%d",&a[i]),M[a[i]]=i; //M数组用以对应a[i]的序号
for(int i=1;i<=m+1;++i)
scanf("%d",&b[i]);
for(int i=1;i<=m+1;++i)
b[i]=M[b[i]]; //用M[]对应转化第二个序列
for(int i=1;i<=m+1;++i)
{
if(!b[i]) continue;
if(b[i]>sstack[top]) sstack[++top]=b[i];
else
{
int l=1,r=top;
while(l<r)
{
int mid=(l+r)>>1;
if(b[i]>sstack[mid]) l=mid+1;
else r=mid;
}
sstack[l]=b[i];
}
}
printf("Case %d:%d",k,top);
}
}
12.木棍加工
传送门
求 l和w同时单调下降的序列 的最小个数
显然先按照一个关键字排序,此时另一个关键字下降子序列的最小划分即为答案
下降子序列的最小划分
=
=
=最长上升子序列 (反之亦然)拦截导弹还记得吧
懒得打
n
l
o
g
n
nlog_n
nlogn就直接
n
2
n^2
n2了
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+10;
int n,up[N];
struct stick
{
int l,w;
} a[N];
bool cmp(stick x,stick y)
{ return x.l>y.l; }
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
cin>>a[i].l>>a[i].w;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;++i)
up[i]=1; //初始化边界值差点又忘了
for(int i=n;i>=1;--i)
for(int j=i+1;j<=n;++j)
if(a[i].w<a[j].w)
up[i]=max(up[i],up[j]+1);
//求最长上升子序列 即 下降子序列的最小划分
int ans=0;
for(int i=1;i<=n;++i)
ans=max(ans,up[i]);
cout<<ans;
}
13.跨河
(River Crossing S)
传送门
f
[
i
]
f[i]
f[i]表示
i
i
i头奶牛的最小花费
从把
j
j
j从
i
i
i枚举到
n
n
n,
则
f
[
i
]
=
m
i
n
(
f
[
i
]
,
f
[
i
−
j
]
+
c
o
s
t
[
j
]
)
f[i]=min(f[i],f[i-j]+cost[j])
f[i]=min(f[i],f[i−j]+cost[j])
注意cost要算上划过去和划回来
但是最后不用划回来,所以答案要减去m
#include<bits/stdc++.h>
using namespace std;
const int N=2e3+5e2+10,M=1e3+10;
int n,m[N],f[N];
int main()
{
cin>>n>>m[0];
for(int i=1;i<=n;++i)
{
int x;
cin>>x;
m[i]=m[i-1]+x;
}
memset(f,0x7f,sizeof(f));
f[0]=0; //差点又tm忘了边界值
for(int i=1;i<=n;++i)
for(int j=1;j<=i;++j)
f[i]=min(f[i],f[i-j]+m[j]+m[0]);
cout<<f[n]-m[0];
}
另外此题可当做背包来做
运送不同数量的奶牛
=
=
=不同的物品
奶牛的数量
=
=
=背包的容量
具体看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e3+5e2+10,M=1e3+10;
int n,m[N],f[N];
int main()
{
cin>>n>>m[0];
for(int i=1;i<=n;++i)
{
int x;
cin>>x;
m[i]=m[i-1]+x;
}
memset(f,0x7f,sizeof(f));
f[0]=0; //差点又tm忘了边界值
for(int i=1;i<=n;++i)
for(int j=i;j<=n;++j)
f[j]=min(f[j],f[j-i]+m[i]+m[0]);
cout<<f[n]-m[0];
}
14.照明系统设计
(Lighting System Design)
传送门(luogu)
传送门(VJ)
电压大的灯泡替换电压小的灯泡 显然要先以灯泡的电压为关键字排序
对于某一段灯泡
k
k
k~~
j
j
j(
j
≤
i
j\le i
j≤i) ,全部由i灯泡替换 一定优于 部分由i灯泡替换,部分由其他替换
所以用
f
[
i
]
f[i]
f[i]表示前
i
i
i个灯泡的最优解,则转移方程:
f
[
i
]
=
m
i
n
(
f
[
i
]
,
f
[
j
]
+
c
[
i
]
∗
(
j
到
i
的
灯
泡
总
个
数
)
+
k
[
i
]
)
(
j
<
i
)
f[i]=min(f[i],f[j]+c[i]*(j到i的灯泡总个数)+k[i])\ \ \ \ (j<i)
f[i]=min(f[i],f[j]+c[i]∗(j到i的灯泡总个数)+k[i]) (j<i)
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int n,f[N],sum[N];
struct light
{
int v,k,c,l;
} a[N]; //用结构体记录四个关键字
bool cmp(light x,light y)
{ return x.v<y.v; }
int main()
{
while(cin>>n,n)
{
for(int i=1;i<=n;++i)
scanf("%d%d%d%d",&a[i].v,&a[i].k,&a[i].c,&a[i].l);
sort(a+1,a+1+n,cmp); //以灯泡的电压为关键字排序
for(int i=1;i<=n;++i)
sum[i]=sum[i-1]+a[i].l; //求灯泡总个数的前缀和
memset(f,0x7f,sizeof(f));
f[0]=0; //我说我又把边界值搞忘了你信吗
for(int i=1;i<=n;++i)
for(int j=0;j<i;++j)
f[i]=min(f[i],f[j]+a[i].k+a[i].c*(sum[i]-sum[j]));
cout<<f[n]<<endl;
}
}
15. 出租车拼车
传送门
这题依旧是一个背包 出租车是物品 人数是背包容量 但是还需要比普通的背包加一维枚举第
i
i
i辆车坐几个人(注意取min,防止下标为负)
则转移方程:
f
[
j
]
=
m
i
n
(
f
[
j
]
,
f
[
j
−
k
]
+
t
[
i
]
∗
k
+
D
)
f[j]=min(f[j],f[j-k]+t[i]*k+D)
f[j]=min(f[j],f[j−k]+t[i]∗k+D)
#include<bits/stdc++.h>
using namespace std;
const int N=1e2+10;
int n,m,d,s;
int f[N];
struct car
{
int t,z;
} a[N];
int main()
{
cin>>n>>m>>d>>s;
for(int i=1;i<=m;++i)
cin>>a[i].t>>a[i].z;
memset(f,0x7f,sizeof(f));
f[0]=0; //dp边界值 这回我记住了
for(int i=1;i<=m;++i)
for(int j=n;j>=1;--j) //相当于01背包 一维滚动数组必须倒序循环
{
int num=min(a[i].z,j);
//num表示乘i车最多能走多少人 (避免下标为负)
for(int k=1;k<=num;++k)
f[j]=min(f[j],f[j-k]+d+k*a[i].t);
}
if(f[n]>30000) cout<<"impossible";
else cout<<f[n];
}
16.最佳课题选择
传送门
背包问题,论文
=
=
=背包容量,课题
=
=
=物品
由于一个课题可以占用多个论文,所以需要加上一层循环枚举此课题占用多少论文
所以这不就熟多重背包吗
但是注意,此题因为权值的计算方式 占用不同数量论文的同一个课题不能简单的相加(占用一个论文 和 占用两个论文 相加不能等同于 占用三个论文)
所以
k
k
k循环必须放在第三层,二进制拆分这题是不能用的
我就挂在这个神奇的地方
#include<bits/stdc++.h>
using namespace std;
const int N=210,M=30;
int n,m,a[M],b[M];
long long f[N];
long long B(int x,int y)
{
long long ans=1;
for(int i=1;i<=y;++i)
ans*=x;
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i)
scanf("%d%d",&a[i],&b[i]);
memset(f,0x7f,sizeof(f));
f[0]=0; //边界值!!!
for(int i=1;i<=m;++i)
for(int j=n;j>=k;--j)
for(int k=1;k<=j;++k) //枚举放入几个i课题
f[j]=min(f[j],f[j-k]+(long long)a[i]*B(k,b[i]));
printf("%lld",f[n]);
}
17.奶牛零食
(Treats for the Cows G/S)
传送门
每次卖出数列第一个或者最后一个,那么转移方程就很显然了
用
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示区间
[
i
,
j
]
[i,j]
[i,j]最大的收益,则:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
+
1
]
[
j
]
+
v
a
l
[
i
]
,
f
[
i
]
[
j
−
1
]
+
v
a
l
[
j
]
)
f[i][j]=max(f[i+1][j]+val[i],f[i][j-1]+val[j])
f[i][j]=max(f[i+1][j]+val[i],f[i][j−1]+val[j])
而
v
a
l
[
i
]
val[i]
val[i]与
i
i
i卖出的时间有关的,时间是可以计算得到的:
d
a
y
=
n
−
(
j
−
i
+
1
)
+
1
day=n-(j-i+1)+1
day=n−(j−i+1)+1
于是就变成了区间DP:下面是我自己对区间dp的理解
在枚举所有区间时,为了保证小区间比大区间先更新(因为需要通过小区间更新大区间)
先从小到大枚举区间的长度,再枚举区间起点(长度为1的区间即是边界值)
注:与一般的 两层
1
1
1至
n
n
n循环相比,复杂度相同,但枚举顺序不同
#include<bits/stdc++.h>
using namespace std;
const int N=2e3+10;
int n,val[N],f[N][N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&val[i]);
memset(f,0x7f,sizeof(f));
for(int i=1;i<=n;++i)
f[i][i]=val[i]*n; //边界值即为最后只剩一个的区间
for(int len=2;len<=n;++len)
for(int i=1,j=i+len-1;j<=n;++i,++j)//这两层循环就是区间dp吧
{ //便于理解,这里仍然用i,j来表示区间(当然也可以用 i,len组成的的式子 做下标)
int day=n-len+1; //day表示是第几天卖出的食物
f[i][j]=max(f[i+1][j]+val[i]*day,f[i][j-1]+val[j]*day);
}
printf("%d",f[1][n]);
}
18.能量项链
传送门
这题跟石子合并似乎毫无区别…
首先显然是区间dp,然后对于区间
[
i
,
j
]
[i,j]
[i,j],枚举一个
k
k
k把
区
间
[
i
,
j
]
区间[i,j]
区间[i,j]分为
[
i
,
k
]
[i,k]
[i,k]和
[
k
+
1
,
j
]
[k+1,j]
[k+1,j]
则转移方程:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
]
,
f
[
i
]
[
k
]
+
f
[
k
+
1
]
[
j
]
+
h
e
a
d
[
i
]
∗
t
a
i
l
[
k
]
∗
t
a
i
l
[
j
]
)
f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]*tail[k]*tail[j])
f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]∗tail[k]∗tail[j])
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int n,head[2*N],tail[2*N];
int f[2*N][2*N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i)
scanf("%d",&head[i]),head[i+n]=head[i]; //通过把序列增致2*n来拆环成链
for(int i=1;i<=2*n;++i)
tail[i]=head[i+1];//第i颗珠子的尾标记应该等于第i+1颗珠子的头标记
tail[2*n]=head[1]; //第n颗珠子的尾标记应该等于第1颗珠子的头标记
for(int len=2;len<=n;++len)
for(int i=1,j=i+len-1;j<=2*n;++i,++j)
for(int k=i;k<j;++k)
f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]*tail[k]*tail[j]);
int ans=0;
for(int i=1;i<=n;++i)
ans=max(ans,f[i][i+n-1]);
//环拆成双链后,则从1-n任意一个作起点 长度为n的序列,都可能成为答案
cout<<ans;
}
19.跳房子
传送门
这毒瘤题我调了一晚上
看到题面,显然是要用二分答案:二分一个
m
i
d
mid
mid为金币数
c
h
e
c
k
(
m
i
d
)
\ \ check(mid)
check(mid)是否能得到大于k分
然后check()函数应该很容易就想到递推公式了:
f
[
i
]
=
m
a
x
f
[
j
≤
i
且
j
能
走
到
i
]
+
v
a
l
[
i
]
f[i]=max\ f[j\le i且j能走到i]+val[i]
f[i]=max f[j≤i且j能走到i]+val[i]
于是提交,* 50分 *警告,
O
(
n
2
l
o
g
n
)
O(n^2log_n)
O(n2logn)显然太大了
由于我们每次的选择是一定范围内
f
f
f数组的最大值且范围在持续向右挪,所以想到用单调队列维护,则
f
[
i
]
=
f
[
q
.
f
r
o
n
t
(
)
]
+
v
a
l
[
i
]
f[i]=f[q.front()]+val[i]
f[i]=f[q.front()]+val[i]
复杂度是
O
(
n
l
o
g
n
)
,
A
C
O(nlog_n),\ AC
O(nlogn), AC撒花
(本人是用双端队列写的单调队列 手写太难了 )
另外此题实现细节有亿点多 具体看代码
(这题可以对照Pogo的牛学习)
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n;
long long f[N],d,k;
bool vis[N];
struct square
{
long long x,val;
} a[N];
long long read() //读入优化
{
long long x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f; //我每次写读入优化都要忘这个
}
bool check(long long x)
{
long long minn=max((long long)1,d-x),maxx=d+x;//行走步数的左右极值
memset(f,0x80,sizeof(f));
memset(vis,true,sizeof(vis));
f[0]=0;f[n+1]=999999999;
deque<int> q; //我习惯用双端队列来做单调队列
int num=0; //num表示下一个该入队的是num
for(int i=1;i<=n;++i)
{
for( ;num<i;++num)//枚举i之前还未入队的元素
{ //把能够到达i(与i距离在minn以上)的元素的入队
if(a[i].x-a[num].x<minn) break;
if(!vis[num]) continue;
while(!q.empty()&&f[q.back()]<=f[num])
q.pop_back();//队尾弹出f[]比j小的元素
q.push_back(num);
}
while(!q.empty()&&a[q.front()].x+maxx<a[i].x)
q.pop_front(); //队首弹出与i距离太远的元素
if(!q.empty()) f[i]=f[q.front()]+a[i].val;//更新f[i]
else vis[i]=false;
if(f[i]>=k) return true;
}
return false;
}
int main()
{
n=read();d=read();k=read();
for(int i=1;i<=n;++i)
{
a[i].x=read();
a[i].val=read();
}
a[n+1].x=999999999;
bool flag=false; //flag用来判断是否能够获得k分
long long l=1,r=max(d,a[n].x); //l,r是二分左右端点
while(l<r)
{
long long mid=(l+r)>>1;
if(check(mid)) r=mid,flag=true;
else l=mid+1;
}
if(flag) printf("%d",l);
else printf("-1");
}
20.摆花
传送门
这题基本上是裸的多重背包,只不过统计的是所有的方案数,只需要把先前的方案数累加而非取
m
a
x
max
max就行了:
f
[
j
]
=
f
[
j
]
+
f
[
j
−
k
]
f[j]=f[j]+f[j-k]
f[j]=f[j]+f[j−k]
数据范围还很小,你甚至不用二进制优化,直接
O
(
n
m
k
)
O(n\ m\ k)
O(n m k)就过了
#include<bits/stdc++.h>
using namespace std;
const int N=110,M=110,mod=1000007;
int n,m,a[N],f[M];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;++i)
cin>>a[i];
f[0]=1;//边界值
for(int i=1;i<=n;++i)
for(int j=m;j>=1;--j)
for(int k=1;k<=min(a[i],j);++k) //直接多重背包朴素算法
f[j]=(f[j]+f[j-k])%mod;
cout<<f[m];
}
21.摆渡车
传送门
傻子都知道第一步要排序
∗
*
∗那么我们先想想状态怎么转移,很容易想到可以,用
f
[
i
]
f[i]
f[i]表示前
i
i
i个人等待最少时间:
f
[
i
]
=
m
i
n
(
f
[
i
]
,
f
[
j
−
1
]
+
j
至
i
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f[i]=min(f[i],f[j-1]\ +\ j至i
f[i]=min(f[i],f[j−1] + j至i的总等待时间
)
)
)(上一班车最后一个走的是
j
−
1
j-1
j−1)
但是对于
j
至
i
j至i
j至i的等待时间我们无法确定车在是什么时候到的:在
i
i
i来之前就已经到了? 还是
i
i
i来之后才到?那
i
i
i又等了多久?
所以必须还要枚举等待时间,但是
t
[
i
]
t[i]
t[i]的取值范围有点吓人的啊,不可能直接当做循环
观察到每个人等待时间最多不超过m(若超过了则意味着在他开始等之后车来过而他却没上 这不是有毛病吗 )
那么我们可以想到枚举每个人等待的时间,这样最多也就
m
m
m次而已,时间复杂度完全足够
∗
*
∗那么我们就用
f
[
i
]
[
k
]
f[i][k]
f[i][k]表示前i个学生 且 第i个学生等待时间为k 的最小总等待时间 :
f
[
i
]
[
k
]
=
m
i
n
(
f
[
i
]
[
k
]
,
f
[
j
]
[
t
]
+
j
至
i
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f[i][k]=min(f[i][k],f[j][t ]\ +\ j至i
f[i][k]=min(f[i][k],f[j][t] + j至i的总等待时间
)
)
)
四个字母怎么行,我们发现方程中,k与t是有等量关系的:
j
\ \ \ \ j
j等待时间为
t
t
t
⇒
\Rightarrow
⇒
j
j
j所乘车发车时间
t
[
j
]
+
t
t[j]+t
t[j]+t
⇒
\Rightarrow
⇒
j
j
j所乘车下一班即
i
i
i所乘车到达时间
T
=
[
i
]
+
t
+
m
T=[i]+t+m
T=[i]+t+m
⇒
\Rightarrow
⇒
i
i
i等待时间
k
=
m
i
n
(
T
−
[
i
]
,
0
)
k=mi n(T-[i],0)
k=min(T−[i],0)(车到达之后可能直接把
i
i
i接走,或是等
i
i
i来了才接走)
∗
*
∗那转移方程不就出来了:
f
[
i
]
[
m
i
n
(
T
−
[
i
]
,
0
)
]
=
m
i
n
(
f
[
i
]
[
m
i
n
(
T
−
[
i
]
,
0
)
]
,
f
[
j
]
[
t
]
+
j
至
i
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f[i][mi n(T-[i],0)]=min(f[i][mi n(T-[i],0)],f[j][t ]\ +\ j至i
f[i][min(T−[i],0)]=min(f[i][min(T−[i],0)],f[j][t] + j至i的总等待时间
)
)
)
我们可以由
j
j
j的等待时间推出
i
i
i的等待时间,当然也可以由
i
i
i的等待时间推出
j
j
j的等待时间,就看你想要怎么推了
#include<bits/stdc++.h>
using namespace std;
const int N=510,M=110;
int n,m;
long long f[N][M],t[N],sum[N];
//f[i][k]表示前i个学生 且 第i个学生等待时间为k 的最小总等待时间
int main()
{
cin>>n>>m;
for(int i=1;i<=n;++i)
cin>>t[i];
sort(t+1,t+1+n); //先将学生排序
for(int i=1;i<=n;++i)
sum[i]=sum[i-1]+t[i];
/* 预处理前i个学生的可以上车的时间,
则等到t时刻,j~i的总等待时间=t*(i-j+1)-(sum[i]-sum[j-1]) */
memset(f,0x7f,sizeof(f));
for(int i=0;i<m;++i)
f[0][i]=0;//边界值
for(int i=1;i<=n;++i)
for(int j=1;j<=i;++j)
{//学生j~i 在i可以上车之后共同上车
int maxx=min((long long)m,t[i]-t[j-1]);//此时j-1必须已经走了
for(int k=0;k<=maxx;++k)
{//枚举学生j-1等待的时间(最多为m-1),可计算出此次发车的时间
int T; //T表示此次发车时刻
if(j==1) T=max(t[j-1]+k,t[i]);//j-1=0时,是不用算上车回来的时间的
else T=max(t[j-1]+k+m,t[i]);
long long all=f[j-1][k]+T*(i-j+1)-(sum[i]-sum[j-1]);
f[i][T-t[i]]=min(all,f[i][T-t[i]]);
}
}
long long ans=999999999;
for(int i=0;i<m;++i)
ans=min(ans,f[n][i]);
cout<<ans;
}
22.POGO的牛
(Pogo-Cow S)
传送门
这题乍一看跟跳房子有点像,但是那道题每一步的距离是确定在某一范围内,而这道题是每一步都要大于等于上一步,想要直接枚举满足范围的步数就十分困难
那么我们就直接将
i
i
i的状态由所有的
j
(
j
<
i
)
j(j<i)
j(j<i)的状态转移而来,而不去枚举步数范围
我们就用
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示第
i
i
i个点从
j
j
j跳过来的最优解,则:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
j
]
[
k
]
)
+
p
[
i
]
)
(
k
<
j
<
i
)
f[i][j]=max(f[j][k])+p[i])\ \ (k<j<i)
f[i][j]=max(f[j][k])+p[i]) (k<j<i)
但是这样是三层循环
O
(
n
3
)
O(n^3)
O(n3),于是就炸了
再想想跳房子我们是用一个单调队列维护每一步可行范围内的最大值
而这道题我们要找的是
m
a
x
(
f
[
j
]
[
k
]
)
max(f[j][k])
max(f[j][k]),如果将
j
j
j定住
i
i
i向右移,则此时满足范围的&k&是向左移的
每次向左移我们就取一个
m
a
x
max
max,那我们就能直接得到
m
a
x
(
f
[
j
]
[
k
]
)
max(f[j][k])
max(f[j][k])了
所以最外层循环我们得先放
j
j
j,定住
j
j
j后再向右移
i
i
i,并用一个指针去移
k
k
k,这样就把复杂度压到
O
(
n
2
)
O(n^2)
O(n2)了
(另外,由于这道题可以往俩个方向走,所以需要两次dp,一次向前,一次向后)
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int n,f[N][N],ans;
struct position
{
int x,p;
} a[N];
bool cmp(position x,position y)
{ return x.x<y.x; }
int main()
{
cin>>n;
for(int i=1;i<=n;++i)
cin>>a[i].x>>a[i].p;
sort(a+1,a+1+n,cmp); //按照位置排序
//正着dp///
for(int i=1;i<=n;++i)
f[i][i]=a[i].p;//边界值
for(int j=1;j<=n;++j)
{
int k=j,max_j_k=0;
for(int i=j+1;i<=n;++i)
{
while(k&&(a[i].x-a[j].x>=a[j].x-a[k].x)) //如果k能往前移
max_j_k=max(max_j_k,f[j][k]),--k;
f[i][j]=max_j_k+a[i].p;
ans=max(ans,f[i][j]);
}
}
//倒着dp///
for(int i=1;i<=n;++i)
f[i][i]=a[i].p;//边界值
for(int j=n;j>=1;--j)
{
int k=j,max_j_k=0;
for(int i=j-1;i>=1;--i)
{
while(k<=n&&(a[j].x-a[i].x>=a[k].x-a[j].x)) //如果k能往前移
max_j_k=max(max_j_k,f[j][k]),++k;
f[i][j]=max_j_k+a[i].p;
ans=max(ans,f[i][j]);
}
}
///
cout<<ans;
}
23.乌龟棋
传送门
既然四种卡片的数量最多也就40张,那么我们直接简单的枚举4种卡片的数量就行了
这有点类似于背包问题,我们还需要枚举走到了哪里
所以转移方程:
f
[
i
]
[
a
]
[
b
]
[
c
]
[
d
]
=
m
a
x
(
f
[
p
o
s
]
[
a
]
[
b
]
[
c
]
[
d
]
,
f
[
p
o
s
−
k
]
[
a
−
1
]
[
b
]
[
c
]
[
d
]
.
.
.
[
b
−
1
]
.
.
.
[
c
−
1
]
.
.
.
[
d
−
1
]
)
f[i][a][b][c][d]=max(f[pos][a][b][c][d],f[pos-k][a-1][b][c][d]...[b-1]...[c-1]...[d-1])
f[i][a][b][c][d]=max(f[pos][a][b][c][d],f[pos−k][a−1][b][c][d]...[b−1]...[c−1]...[d−1])
但是这样不仅空间爆炸,时间也过不了
题目说卡牌的数量刚好满足走到终点,那走到的位置就可以直接使用的不同卡牌的数量来表示了:
p
o
s
=
a
+
b
∗
2
+
c
∗
3
+
d
∗
4
+
1
pos=a+b*2+c*3+d*4+1
pos=a+b∗2+c∗3+d∗4+1 (+1是因为从位置1出发)
#include<bits/stdc++.h>
using namespace std;
const int N=500,M=200;
int n,m,val[N],cnt[5];
int f[50][50][50][50];
int find_pos(int a,int b,int c,int d)
{ return a+2*b+3*c+4*d; }
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&val[i]);
for(int i=1;i<=m;++i)
{
int x; scanf("%d",&x);
cnt[x]++; //记录不同卡牌的数量
}
f[0][0][0][0]=val[1]; //边界值
for(int a=0;a<=cnt[1];++a)
for(int b=0;b<=cnt[2];++b)
for(int c=0;c<=cnt[3];++c)
for(int d=0;d<=cnt[4];++d)
{
int pos=find_pos(a,b,c,d)+1;//因为刚开始就在起点1了,所以pos要+1
if(a) f[a][b][c][d]=max(f[a][b][c][d],f[a-1][b][c][d]+val[pos]);
if(b) f[a][b][c][d]=max(f[a][b][c][d],f[a][b-1][c][d]+val[pos]);
if(c) f[a][b][c][d]=max(f[a][b][c][d],f[a][b][c-1][d]+val[pos]);
if(d) f[a][b][c][d]=max(f[a][b][c][d],f[a][b][c][d-1]+val[pos]);
}
int ans;
ans=f[cnt[1]][cnt[2]][cnt[3]][cnt[4]];//所有卡牌用完时,即到达终点时,为答案
printf("%d",ans);
}
24.跑步
(Running S)
传送门
这题转移方程题目基本上都说的很明白了
需要注意的是由于若于某个时刻疲劳值恢复至
0
0
0,无法确定是什么时刻开始休息的
所以这道题我们采用由前面推至后面的方式,即所谓的填表,而不是刷表
用
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示
i
i
i分钟,疲劳值为
j
j
j的最大跑步距离:
如果在第
i
i
i分钟内跑步,她可以在这一分钟内跑
d
i
d_i
di 米,并且疲劳值
+
1
+1
+1:
f
[
i
+
1
]
[
j
+
1
]
=
m
a
x
(
f
[
i
+
1
]
[
j
+
1
]
,
f
[
i
]
[
j
]
+
d
[
i
+
1
]
)
f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+d[i+1])
f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+d[i+1])
如果选择休息,那么她的疲劳度就会每分钟减少
1
1
1,但她必须休息到疲劳度恢复到
0
0
0 为止:
f
[
i
+
j
]
[
0
]
=
m
a
x
(
f
[
i
+
j
]
[
0
]
,
f
[
i
]
[
j
]
)
f[i+j][0]=max(f[i+j][0],f[i][j])
f[i+j][0]=max(f[i+j][0],f[i][j])
在疲劳度为
0
0
0 时休息的话,疲劳度不会再变动:
f
[
i
+
1
]
[
j
]
=
m
a
x
(
f
[
i
+
1
]
[
j
]
,
f
[
i
]
[
j
]
)
(
j
=
0
)
f[i+1][j]=max(f[i+1][j],f[i][j])\ \ \ (j=0)
f[i+1][j]=max(f[i+1][j],f[i][j]) (j=0)
最后疲劳值恢复至
0
0
0,所以答案为
f
[
n
]
[
0
]
f[n][0]
f[n][0]
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5e2+10,M=5e2+10;
//注意N的范围,在第二个方程里面有一个f[i+j][0],必须加上500,否则会RE
int n,m,d[N],f[N][M];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)
scanf("%d",&d[i]);
for(int i=0;i<n;++i)
for(int j=0;j<=min(m,i);++j)//i时刻疲劳值不可能超过i
{
if(!j) f[i+1][0]=max(f[i+1][0],f[i][0]);
//在i时刻休息且疲劳值已经为0,不再下降
else f[i+j][0]=max(f[i+j][0],f[i][j]);
//在i时刻休息至疲劳值为0
if(j!=m) f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+d[i+1]);
//在i时刻此时疲劳值没满,可向前跑
}
printf("%d",f[n][0]);//答案要求结束时疲劳值是0
}
25.过河
传送门
这题转移方程太显然:
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
i
−
k
]
)
+
1
/
0
)
(
取
决
于
i
是
否
有
石
子
)
f[i]=max(f[i],f[i−k])+1/0) (取决于i是否有石子)
f[i]=max(f[i],f[i−k])+1/0)(取决于i是否有石子)
但是
L
≤
1
e
9
L\le 1e9
L≤1e9就很迷,不能直接枚举,而
m
m
m又才
100
100
100
于是我想到了用分块,原理很简单,但是调了我一晚上
欢迎博客欣赏(本来想在洛谷写篇题解的结果题解已经交满了)
而这里大众的做法是进行路径压缩,石子
i
i
i与
j
j
j的距离
d
i
s
[
i
]
=
(
x
[
i
]
−
x
[
j
]
)
m
o
d
l
c
m
(
S
.
.
.
T
)
dis[i]=(x[i]-x[j])mod\ lcm(S...T)
dis[i]=(x[i]−x[j])mod lcm(S...T)
(若不想求
l
c
m
lcm
lcm可直接
m
o
d
2520
=
l
c
m
(
1
,
2
,
.
.
.
,
10
)
mod\ 2520=lcm(1,2,...,10)
mod 2520=lcm(1,2,...,10))
#include<bits/stdc++.h>
using namespace std;
int a[104];
int re[104];
int rock[1000005];
int f[1000005];
int L,s,t,m;
int main()
{
cin>>L;
cin>>s>>t>>m;
for(int i=1;i<=m;i++)
cin>>a[i];
sort(a+1,a+m+1);
for(int i=1;i<=m;i++)
re[i]=(a[i]-a[i-1])%2520; //路径压缩
for(int i=1;i<=m;i++)
a[i]=a[i-1]+re[i],rock[a[i]]=1; //按照压缩后的lu
L=a[m];
for(int i=0;i<=L+t;i++)
f[i]=m;
f[0]=0;
for(int i=1;i<=L+t;i++)
for(int j=s;j<=t;j++)
{
if(i-j>=0)
f[i]=min(f[i],f[i-j]);
f[i]+=rock[i];
}
int res=m;
for(int i=L;i<L+t;i++)
res=min(res,f[i]);
cout<<res<<endl;
return 0;
}
26.牛的词汇
(The Cow Lexicon S)
传送门
这题大概算是字符串题目,主要考查字符串处理,暴力dp就行了
用
f
[
i
]
f[i]
f[i]表示母串前
i
i
i个最少需要删除的字母,
c
h
e
c
k
(
i
,
j
)
check(i,j)
check(i,j)前
i
i
i个若以
j
j
j为后缀需要删除的字母个数,则:
若
j
能
是
i
的
后
缀
:
f
[
i
]
=
m
i
n
(
f
[
i
]
,
f
[
i
−
l
e
n
[
j
]
−
c
h
e
c
k
(
i
,
j
)
]
+
c
h
e
c
k
(
i
,
j
)
若
不
能
是
i
的
后
缀
:
f
[
i
]
=
m
i
n
(
f
[
i
]
,
f
[
i
−
1
]
+
1
)
若j能是i的后缀:f[i]=min(f[i],f[i-len[j]-check(i,j)]+check(i,j)\\ 若不能是i的后缀:f[i]=min(f[i],f[i-1]+1)
若j能是i的后缀:f[i]=min(f[i],f[i−len[j]−check(i,j)]+check(i,j)若不能是i的后缀:f[i]=min(f[i],f[i−1]+1)
#include<bits/stdc++.h>
using namespace std;
int n,w,f[301];
char s[301],ch[601][26];
int check(int i,int j,int len)
{
for(int q=len,k=i;k>=1;--k)
{
if(s[k]==ch[j][q]) q--; //每匹配一个字母则q--
if(!q) return i-k+1-len;//若q为0,则表示j串已经匹配成功
}
return -1;
}
int main()
{
scanf("%d%d%s",&n,&w,s+1);
for(int i=1;i<=n;++i)
scanf("%s",ch[i]+1);
memset(f,0x7f,sizeof(f));f[0]=0; //边界值
for(int i=1;i<=w;++i)
{
for(int j=1;j<=n;++j)
{
int len=strlen(ch[j]+1);
if(i>=len&&s[i]==ch[j][len])//至少满足最后一个字母要相同
{
int num=check(i,j,len);
if(num!=-1) f[i]=min(f[i],f[i-len-num]+num);
}
}
f[i]=min(f[i],f[i-1]+1);
}
printf("%d",f[w]);
}
27.奶牛道路
(Cow Traffic S)
传送门
题目要求有向图中所有路径 (入度为
0
0
0的点到
n
n
n点) 经过同一条道路的最大次数
很显然,用
f
1
[
i
]
f_1[i]
f1[i]表示从草场跑到
i
i
i点的路径条数
f
2
[
i
]
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f_2[i]
f2[i]表示从
i
i
i点跑到
n
n
n点的路径条数
则对于
e
d
g
e
u
−
v
edge\ u-v
edge u−v , 经过次数
a
n
s
[
i
]
=
f
1
[
u
]
∗
f
2
[
v
]
\ ans[i]=f_1[u]*f_2[v]
ans[i]=f1[u]∗f2[v]
而
f
1
f
2
f_1\ f_2
f1 f2则可以简单地树形
d
p
dp
dp跑出,为了直观一点呈现我们建正图+反图分别跑
f
2
f
1
f_2\ f_1
f2 f1
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=1e6+10;
int n,m,out[N];
int edge_cnt1,nxt1[M],pos1[M],to1[M],first1[N];
void add1(int x,int y)
{//反图
nxt1[++edge_cnt1]=first1[x];
first1[x]=edge_cnt1;
pos1[edge_cnt1]=x;
to1[edge_cnt1]=y;
}
int edge_cnt2,nxt2[M],pos2[M],to2[M],first2[N];
void add2(int x,int y)
{//正图
nxt2[++edge_cnt2]=first2[x];
first2[x]=edge_cnt2;
pos2[edge_cnt2]=x;
to2[edge_cnt2]=y;
}
int f1[M];
void dfs1(int x)
{//从牧场到x点的方案数
for(int e=first1[x];e;e=nxt1[e])
{
if(!f1[to1[e]]) dfs1(to1[e]);
f1[x]+=f1[to1[e]];
}
return;
}
int f2[M];
void dfs2(int x)
{//从n点到x点的方案数
for(int e=first2[x];e;e=nxt2[e])
{
if(!f2[to2[e]]) dfs2(to2[e]);
f2[x]+=f2[to2[e]];
}
return;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i)
{
int x,y; scanf("%d%d",&x,&y);
out[y]++; //在反图中,放牧地点的出度为0
add1(y,x);
add2(x,y);
}
for(int i=1;i<=n;++i)
if(!out[i]) f1[i]=1; //f1边界值
dfs1(n);
f2[n]=1;
for(int i=1;i<=n;++i)//f2边界值
if(!out[i]) dfs2(i);
int ans=0;
for(int i=1;i<=edge_cnt1;i++)
ans=max(ans,f1[to1[i]]*f2[pos1[i]]); //i=n时不算入
printf("%d",ans);
return 0;
}
28.挤奶时间
(Milking time S)
传送门
由于只有把任务做完的时刻才会对答案做出贡献,
所以对于每个时刻有两种情况:
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
i
−
1
]
)
f[i]=max(f[i],f[i-1])
f[i]=max(f[i],f[i−1]) 不做任务
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
s
t
[
i
]
−
R
]
)
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ f[i]=max(f[i],f[st[i]-R])
f[i]=max(f[i],f[st[i]−R]) 此时刻刚刚做完任务
而每个时刻可能会是多个任务的结束时间,我用了邻接表来储存任务
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10,M=1e3+10;
int n,m,r,f[N];
int edge_cnt,first[N],nxt[M],to[M],s[M];
void add(int x,int y,int z)
{
nxt[++edge_cnt]=first[x];
first[x]=edge_cnt;
to[edge_cnt]=y;
s[edge_cnt]=z;
}
int main()
{
scanf("%d%d%d",&n,&m,&r);
for(int i=1;i<=m;++i)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(y,x,z);
}
for(int i=1;i<=n;++i)
{
for(int e=first[i];e;e=nxt[e])//枚举以此时刻为结束时间的任务
f[i]=max(f[i],f[max(0,to[e]-r)]+s[e]);//注意f[]下标不能为负
f[i]=max(f[i],f[i-1]); //此时刻不做任务
}
printf("%d",f[n]);
}
29.装箱问题
传送门
明显的
01
01
01背包,只不过重量和价值都由容积体现
考虑直接用
f
[
i
]
f[i]
f[i]表示
i
i
i体积下剩余的最小空间
由于刚开始全是空的,所以初值
f
[
i
]
=
i
f[i]=i
f[i]=i,然后就是背包了
#include<bits/stdc++.h>
using namespace std;
const int V=2e4+10,N=30;
int volum,n;
int v[N],f[V];
int main()
{
scanf("%d%d",&volum,&n);
for(int i=1;i<=n;++i)
scanf("%d",&v[i]);
for(int i=1;i<=volum;++i)
f[i]=i; //边界值即为全部为空
for(int i=1;i<=n;++i)
for(int j=volum;j>=v[i];--j)
f[j]=min(f[j],f[j-v[i]]);
printf("%d",f[volum]);
}
30.打鼹鼠
传送门
这题难点在于用什么来
d
p
dp
dp,时间,坐标都不好处理
于是想到我们可以直接
d
p
dp
dp 鼹鼠的编号:用
f
[
i
]
f[i]
f[i]表示打掉第
i
i
i只鼹鼠能得到的最大贡献
那么
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
j
]
+
1
)
f[i]=max(f[i],f[j]+1)
f[i]=max(f[i],f[j]+1)
由于
f
[
i
]
f[i]
f[i]表示的是 打掉
i
i
i,当然答案可能不打掉,所以
a
n
s
=
m
a
x
(
a
n
s
,
f
[
i
]
)
ans=max(ans,f[i])
ans=max(ans,f[i])
题目甚至已经排好了序
#include<bits/stdc++.h>
#define time Time //用了万能头,time是关键字
using namespace std;
const int N=1e3+10,M=1e4+10;
int n,m,f[M],time[M],x[M],y[M];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i)
scanf("%d%d%d",&time[i],&x[i],&y[i]);
for(int i=1;i<=m;++i)
f[i]=1; //由于初始位置自选 ,边界值为1
for(int i=1;i<=m;++i)
for(int j=1;j<i;++j)
if(abs(x[i]-x[j])+abs(y[i]-y[j])<=time[i]-time[j])
f[i]=max(f[i],f[j]+1);
int ans=0;
for(int i=1;i<=m;++i)
ans=max(ans,f[i]);
printf("%d",ans);
}
31.道路游戏
传送门
此题时间,工厂序号,行走步数共三维
由于不管
k
k
k值取多少,每次行动都在到达终点(指废弃机器人的地方)时产生贡献,我们依旧用时间进行
d
p
dp
dp,
而每次新造机器人时是可以随意选择位置的,因此只需要一维
f
[
i
]
f[i]
f[i]表示时间为
i
i
i时贡献最大值,无须考虑上次行动的终点
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
i
−
k
]
−
c
o
s
t
[
j
−
k
]
+
s
u
m
)
f[i]=max(f[i],f[i-k]-cost[j-k]+sum)
f[i]=max(f[i],f[i−k]−cost[j−k]+sum),即指
i
i
i时到达工厂
j
j
j,已经走了
k
k
k步
(当然也可以正着向后推)
于是
O
(
n
m
2
)
O(nm^2)
O(nm2)的优秀复杂度
A
C
AC
AC了 数据过于水
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10,M=1e3+10;
int n,m,p;
int cost[N],val[N][M],f[M];
int pos(int x,int y)
{
int ans=x+y;
if(ans<=0) ans+=n;
if(ans>n) ans-=n;
return ans;
}
int main()
{
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
scanf("%d",&val[i][j]);
for(int i=1;i<=n;++i)
scanf("%d",&cost[i]);
memset(f,0x80,sizeof(f));
f[0]=0;//边界值
for(int i=1;i<=m;++i)
for(int j=1;j<=n;++j)
{//时间为i,到达工厂j
int last,sum=0;
for(int k=1;k<=min(i,p);++k)//i-k>=0
{//last表示k步前买机器人的工厂,sum表示k步共产生的贡献
last=pos(j,-k);sum+=val[last][i-k+1];
f[i]=max(f[i],f[i-k]-cost[last]+sum);
}
}
printf("%d",f[m]);
}
作为一名有追求的
O
I
e
r
OIer
OIer自然是要追求正解复杂度了…(尝试过后)屮做不来,不做了,AC万岁
…龟速更新中