文章目录
习题1 Longest Run on a Snowboard UVA - 10285 (记忆化)
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int dp[N][N],h[N][N];
int r,c;
int DP(int x,int y){
if(dp[x][y])return dp[x][y];
int fl=1;
for(int i=0;i<4;i++){
int nx=x+dx[i],ny=y+dy[i];
if(nx<1||ny<1||nx>r||ny>c)continue;
if(h[nx][ny]<h[x][y]){
fl=0;//当前不是最小的位置
dp[x][y]=max(DP(nx,ny)+1,dp[x][y]);
}
}
if(fl)dp[x][y]=1;
return dp[x][y];
}
int main(){
int T;cin>>T;
while(T--){
string name;
cin>>name>>r>>c;
memset(dp,0,sizeof dp);
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++)
cin>>h[i][j];
}
int ans=0;
for(int i=1;i<=r;i++){
for(int j=1;j<=c;j++)
ans=max(ans,DP(i,j));
}
cout<<name<<": "<<ans<<endl;
}
return 0;
}
习题2 Free Candies UVA - 10118 (记忆化)
题目链接
参考题解
状态设计: 现在有4堆糖果,每次只有4种选择,从4堆糖果中的哪堆拿1个糖果。原来我设
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示现在在第
i
i
i次从第
j
j
j堆糖果拿的最大答案,但是发现这样不够表示状态,因为在拿第
j
j
j堆的时候还需要知道它现在最顶端的位置,因为
n
n
n为40,足够开4维,用
d
p
[
k
1
]
[
k
2
]
[
k
3
]
[
k
4
]
dp[k1][k2][k3][k4]
dp[k1][k2][k3][k4]表示取到第i堆糖果已经取了
k
i
k_i
ki颗时的最大答案。
状态转移: 用记忆化搜索,因为
k
1
,
k
2
,
k
3
,
k
4
k1,k2,k3,k4
k1,k2,k3,k4的顺序无法确定,记忆化搜索可以解决无序的状态转移。
- 篮子已经满了,已经不能再拿糖果,这个状态也不能向别的状态转移,答案也不会增加,直接得到0.
- 篮子未满,新加的糖果颜色在原来的篮子中出现过:将该颜色糖果从篮子里拿走。
- 篮子未满,新加的糖果颜色在原来的篮子中未出现过:直接加入该颜色。
#include<bits/stdc++.h>
using namespace std;
const int N=50;
int color[4][N], dp[N][N][N][N], n;
int solve(int num[],int basket[],int cnt){
int &ans=dp[num[0]][num[1]][num[2]][num[3]];
if(ans!=-1)return ans;
ans=0;
if(cnt==5)return ans;
for(int i=0;i<4;i++){
if(num[i]>=n)continue;
int cur=color[i][num[i]];
if(basket[cur]){
basket[cur]=0,num[i]++;
ans=max(ans,solve(num,basket,cnt-1)+1);
basket[cur]=1,num[i]--;
}
else{
basket[cur]=1,num[i]++;
ans=max(ans,solve(num,basket,cnt+1));
basket[cur]=0,num[i]--;
}
}
return ans;
}
int main(){
while(cin>>n&&n){
for(int j=0;j<n;j++)for(int i=0;i<4;i++)cin>>color[i][j];
memset(dp,-1,sizeof dp);
int basket[21]={0},num[4]={0};
cout<<solve(num,basket,0)<<endl;
}
return 0;
}
习题3 Cake slicing UVA - 1629 (记忆化)
题目链接
每次可以在一块蛋糕上面切1刀,那块蛋糕会被分成两块,只要每块上面还有樱桃就是合法的,假设切的蛋糕长和宽为
x
,
y
x,y
x,y,可以横切和纵切,一共有
x
−
1
+
y
−
1
x-1+y-1
x−1+y−1种切法,将当前的蛋糕从蛋糕集合里删除,再加入两块新的切好的蛋糕继续切。还是采用记忆化搜索,因为切好的蛋糕顺序不定。最终状态是整个蛋糕都切好,起始状态是某块🎂只有1个🍒的时候,不用再切,直接返回0。
状态设计: dp[a][b][c][d]表示以(a,b)为左上角坐标,(c,d)为右下角坐标的矩形达到其中每小块只有1个樱桃至少需要切的长度。但是矩形除了坐标还需要知道其中的樱桃个数,只需要用一个二维前缀和数组就可以在O(1)时间计算某个矩形的樱桃个数以确定是否需要继续切割。
状态转移:
- 当前蛋糕只有1个樱桃,返回0。
- 枚举当前蛋糕横切和纵切。
#include<bits/stdc++.h>
using namespace std;
const int N=25;
const int inf=0x3f3f3f3f;
int dp[N][N][N][N],g[N][N],sum[N][N];//dp[lx][ly][rx][ry]为该矩形切割完成后的最少切割长度,num表示该矩形内的樱桃数量
int n,m,cherry;
inline int cal(int a,int b,int c,int d){
return sum[c][d]-sum[a-1][d]-sum[c][b-1]+sum[a-1][b-1];
}
int DP(int a,int b,int c,int d){
int &ans=dp[a][b][c][d];
if(ans!=inf)return ans;
if(cal(a,b,c,d)==1)return ans=0;
for(int i=a;i<c;i++)//横切
if(cal(a,b,i,d)&&cal(i+1,b,c,d))
ans=min(ans,DP(a,b,i,d)+DP(i+1,b,c,d)+d-b+1);
for(int i=b;i<d;i++)
if(cal(a,b,c,i)&&cal(a,i+1,c,d))
ans=min(ans,DP(a,b,c,i)+DP(a,i+1,c,d)+c-a+1);
return ans;
}
int main(){
int kase=0;
while(cin>>n>>m>>cherry&&n&&m){
memset(dp,0x3f,sizeof dp);
memset(sum,0,sizeof sum);
memset(g,0,sizeof g);
for(int i=1;i<=cherry;i++){
int x,y;cin>>x>>y;
g[x][y]=1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
if(g[i][j])sum[i][j]++;
}
}
cout<<"Case "<<++kase<<": ";
cout<<DP(1,1,n,m)<<endl;
}
return 0;
}
习题4 Cyborg Genes UVA - 10723
状态设计:
dp1[i][j]表示第一个到第一个串的i位置和第二个串的j位置时,两个串前缀的最大公共子序列。
dp1[i][j]表示第一个到第一个串的i位置和第二个串的j位置时,两个串前缀的最多数量。
第一个状态比较容易想,是LCS(最大公共子序列)的简单变形,要使得后来的新串既包含原来两个串,又要使新串最短,那新串就是在原来两个串的LCS里穿插两个串特有的其他非公共序列。
状态转移:
第二个状态dp2[i][j]完全依赖于第一个状态的转移情况:
- 当前位置两个字符相同,dp2[i][j]=dp2[i-1][j-1],因为现在的LCS就是加上当前位置相同的字符s1[i-1]和s2[j-1](因为是从0开始计数,字符串位置是当前dp枚举位置-1)
#include<bits/stdc++.h>
const int inf=0x3f3f3f3f;
using namespace std;
const int N=40;
int dp1[N][N],dp2[N][N];
string s1,s2;
int main(){
int T;cin>>T;
int kase=0;
getchar();
while(T--){
getline(cin,s1),getline(cin,s2);
memset(dp1,0,sizeof dp1);
memset(dp2,0,sizeof dp2);
int n1=s1.length(),n2=s2.length();
for(int i=0;i<=n1;i++)dp2[i][0]=1;
for(int i=0;i<=n2;i++)dp2[0][i]=1;
for(int i=1;i<=n1;i++){
for(int j=1;j<=n2;j++){
if(s1[i-1]==s2[j-1]){
dp1[i][j]=dp1[i-1][j-1]+1;
dp2[i][j]=dp2[i-1][j-1];
}
else {
dp1[i][j]=max(dp1[i-1][j],dp1[i][j-1]);
if(dp1[i-1][j]>dp1[i][j-1])
dp2[i][j]+=dp2[i-1][j];
else if(dp1[i][j-1]>dp1[i-1][j])
dp2[i][j]+=dp2[i][j-1];
else dp2[i][j]+=dp2[i-1][j]+dp2[i][j-1];
}
}
}
cout<<"Case #"<<++kase<<": "<<n1+n2-dp1[n1][n2]<<' '<<dp2[n1][n2]<<endl;
}
}
习题8 Alibaba UVA - 1632 (区间DP)
虽然觉得时间空间真的宽容到离谱
题目链接
状态设计: 考虑一个区间
(
l
,
r
)
(l,r)
(l,r),当这个区间里的宝藏都被取走,最后肯定从
l
l
l离开或者从
r
r
r离开。因此一个区间还要继续分成两个状态:
d
p
[
l
]
[
r
]
[
0
]
dp[l][r][0]
dp[l][r][0]表示从
l
l
l离开区间,
d
p
[
l
]
[
r
]
[
1
]
dp[l][r][1]
dp[l][r][1]表示从
r
r
r离开此区间。
状态转移:
d
p
[
l
]
[
r
]
[
0
]
=
m
i
n
(
d
p
[
l
+
1
]
[
r
]
[
0
]
+
d
i
s
(
l
,
l
+
1
)
,
d
p
[
l
+
1
]
[
r
]
[
1
]
+
d
i
s
[
l
,
r
]
)
dp[l][r][0]=min(dp[l+1][r][0]+dis(l,l+1),dp[l+1][r][1]+dis[l,r])
dp[l][r][0]=min(dp[l+1][r][0]+dis(l,l+1),dp[l+1][r][1]+dis[l,r])
dp[l][r][0]转移同理。
#include<bits/stdc++.h>
#define fD from+Dis
#define min3(a,b,c) min(a,min(b,c))
const int inf=0x3f3f3f3f;
using namespace std;
const int N=1e4+7;
int dp[N][N][2];
int p[N],d[N];
int n;
inline int dis(int x,int y){return abs(p[x]-p[y]);}
int main(){
while(cin>>n){
for(int i=1;i<=n;i++){
cin>>p[i]>>d[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++)
dp[i][j][0]=dp[i][j][1]=inf;
}
for(int i=1;i<=n;i++)dp[i][i][0]=dp[i][i][1]=0;
for(int len=2;len<=n;len++){//len表示区间内有几个点
for(int i=1;i<=n-len+1;i++){
int l=i,r=i+len-1;
int from=dp[l+1][r][0],Dis=dis(l,l+1);
if(d[l]>fD)dp[l][r][0]=fD;
from=dp[l+1][r][1],Dis=dis(l,r);
if(d[l]>fD)dp[l][r][0]=min(dp[l][r][0],fD);
from=dp[l][r-1][1],Dis=dis(r-1,r);
if(d[r]>fD)dp[l][r][1]=min(dp[l][r][1],fD);
from=dp[l][r-1][0],Dis=dis(l,r);
if(d[r]>fD)dp[l][r][1]=min(dp[l][r][1],fD);
}
}
if(dp[1][n][0]!=inf||dp[1][n][1]!=inf)
cout<<min(dp[1][n][0],dp[1][n][1])<<endl;
else cout<<"No solution"<<endl;
}
}
习题9 Storage Keepers UVA - 10163
题目链接
参考博客
状态设计:
dp1[i][j]表示前i个仓库用了前j个守卫时最小安全系数的最大值。
dp2[i][j]表示前i个仓库用了前j个守卫时最小的花费。
状态转移: 外层枚举守卫,内层枚举仓库。多加一个守卫会贡献守卫值,这个守卫值可以去分摊给后几个人。
- min里第一个表示前k-1个中最小系数的最大值,第二个表示雇佣第i个守卫看守k个仓库的安全系数:
d p 1 [ j ] = m a x ( d p 1 [ j ] , m i n ( d p 1 [ j − k ] , p [ i ] / k ) ) dp1[j]=max(dp1[j],min(dp1[j-k],p[i]/k)) dp1[j]=max(dp1[j],min(dp1[j−k],p[i]/k)) - 取工资最小值。
d p 2 [ j ] = m i n ( d p 2 [ j ] , d p 2 [ j − k ] + p [ i ] ) ; dp2[j]=min(dp2[j],dp2[j-k]+p[i]); dp2[j]=min(dp2[j],dp2[j−k]+p[i]);
#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
int dp[110];
int m,n;//m为仓库 n为守卫个数
int p[40];
int main(){
while(cin>>m>>n&&n&&m){
memset(dp,0,sizeof dp);
dp[0]=inf;
for(int i=1;i<=n;i++)cin>>p[i];
//先计算最小安全系数的最大值
for(int i=1;i<=n;i++){ //枚举守卫
for(int j=m;j>=0;j--){ //枚举仓库
for(int k=1;k<=j&&k<=p[i];k++){ //枚举第i名守卫看守的仓库个数
dp[j]=max(dp[j],min(dp[j-k],p[i]/k));
}
}
}
int ans=dp[m];
cout<<ans<<' ';
if(!ans){
cout<<ans<<endl;continue;
}
memset(dp,0x3f,sizeof dp);
dp[0]=0;
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=min(j,p[i]/ans);k>0;k--){
dp[j]=min(dp[j],dp[j-k]+p[i]);
}
}
}
cout<<dp[m]<<endl;
}
return 0;
}