文章目录
- [例1 A Spy in the Metro UVA - 1025 ](https://vjudge.net/problem/UVA-1025)
- [例2 The Tower of Babylon UVA - 437 ](https://vjudge.net/problem/UVA-437)
- [例3 Tour UVA - 1347 ](https://vjudge.net/problem/UVA-1347)
- [例4 Unidirectional TSP UVA - 116 ](https://vjudge.net/problem/UVA-116)
- [例5 Jin Ge Jin Qu hao UVA - 12563 ](https://vjudge.net/problem/UVA-12563)
- 例6 Another Crisis UVA - 12186
例1 A Spy in the Metro UVA - 1025
状态设计:dp[i][j]表示在第i时刻在第j个站点至少还需要等待的时长。
状态转移:
在当前的车站等待;
坐上向左开的车;
坐上向右开的车。
枚举顺序:第二维的枚举顺序没有关系 因为对于每个j,dp[i+1][j]都已经更新好,最重要的是第一维有序的时间枚举,这也是递推的顺序,即当前时间的状态是从前1秒转移而来。
#include<bits/stdc++.h>
using namespace std;
int n,T,t[100],m1,m2,d1[100],d2[100];
bool has_train[300][100][2];//has_train[i][j][0]表示在第i时刻第j个车站是否有向右边开的列车
int dp[300][100];//dp[i][j]表示在第i时刻在第j个站点至少还需要等待的时长
void solve(){
memset(has_train,0,sizeof has_train);
memset(dp,0x3f,sizeof dp);
cin>>T;
dp[T][n]=0;//已经在此处会面 等待时间为0
for(int i=1;i<n;i++)cin>>t[i];
cin>>m1;
for(int i=1;i<=m1;i++){
cin>>d1[i];int cur=d1[i];
for(int j=1;j<n;j++){
if(cur>T)break;
has_train[cur][j][0]=1;
cur+=t[j];
}
}
cin>>m2;
for(int i=1;i<=m2;i++){
cin>>d2[i];int cur=d2[i];
for(int j=n;j>1;j--){
if(cur>T)break;
has_train[cur][j][1]=1;
cur+=t[j-1];
}
}
for(int i=T-1;i>=0;i--){
for(int j=1;j<=n;j++){
dp[i][j]=min(dp[i][j],dp[i+1][j]+1);
if(has_train[i][j][0]&&i+t[j]<=T&&j+1<=n)
dp[i][j]=min(dp[i][j],dp[i+t[j]][j+1]);
if(has_train[i][j][1]&&j-1>=1&&i+t[j-1]<=T)
dp[i][j]=min(dp[i][j],dp[i+t[j-1]][j-1]);
}
}
}
int main(){
int kase=0;
while(cin>>n&&n){
solve();
cout<<"Case Number "<<++kase<<": ";
if(dp[0][1]!=0x3f3f3f3f)
cout<<dp[0][1]<<endl;
else cout<<"impossible"<<endl;
}
return 0;
}
例2 The Tower of Babylon UVA - 437
每个立方体有6种可以摆放的角度,其实这就是一个最长上升子序列的三维问题。
状态设计:dp[i]表示以编号为i顶端三角形的最大高度
状态转移:dp[i]=max(dp[j]+node[i].z,dp[i])。其中j表示所有底面长宽小于i的立方体编号
代码样例都过不了…在我的理解,无法通过的原因是:当用j去更新i的时候,并无法保证j已经是以j为顶的最大高度,因为排序是无效的,比如a<b,a<c,b<d,c<e,但是无法保证b<e,c<d。
//错误代码!!
#include<bits/stdc++.h>
using namespace std;
struct Node{
int x,y,z;
bool operator<(const Node &a){
if(x<a.x&&y<a.y)return true;
if(x<a.y&&y<a.x)return true;
return false;
}
}node[100];
int dp[100],n;
bool cmp(Node a,Node b){return b<a;}
int solve(){
memset(dp,0,sizeof dp);
for(int i=1;i<=n;i++){
int x,y,z;cin>>x>>y>>z;
node[i]={x,y,z},node[i+n]={y,z,x},node[i+2*n]={x,z,y};
}
int ans=0;
sort(node+1,node+1+n*3,cmp);
for(int i=1;i<=3*n;i++)dp[i]=node[i].z;
for(int i=1;i<=3*n;i++){
for(int j=1;j<=3*n;j++){//如何保证j已经更新完毕呢!即dp[j]表示以j为顶端三角形的最大高度
if(node[i]<node[j])dp[i]=max(dp[j]+node[i].z,dp[i]);
}
}
for(int i=1;i<=3*n;i++)ans=max(dp[i],ans);
return ans;
}
int main(){
int kase=0;
while(cin>>n&&n){
int ans=solve();
cout<<"Case "<<++kase<<": maximum height = "<<ans<<endl;
}
}
参考题解1
正解是DAG上的动态规划。
状态设计: dp[i]表示以i为顶的柱子的最大高度
状态转移: 更新dp[u]时,枚举比u小的立方体i,进行递归计算,dp[i]用的是所有已经到达过最底层的状态去更新的,所以dp[u]一旦算出就是最大值。
#include<bits/stdc++.h>
using namespace std;
const int N=1e2+10;
int n;
int dp[N];
struct Node{
int x,y,z;
bool operator<(const Node &a){
if(x<a.x&&y<a.y)return true;
if(x<a.y&&y<a.x)return true;
return false;
}
}node[N];
int DP(int u){
if(dp[u])return dp[u];
for(int i=1;i<=n;i++){
if(i==u)continue;
if(node[u]<node[i]){
int update=DP(i);
if(update>dp[u])dp[u]=update;
}
}
return dp[u]=dp[u]+node[u].z;
}
int solve(){
memset(dp,0,sizeof dp);
for(int i=1;i<=n;i++){
int x,y,z;cin>>x>>y>>z;
node[i]={y,z,x},node[i+n]={x,z,y},node[i+2*n]={x,y,z};
}
n*=3;
int ans=0;
for(int i=1;i<=n;i++){
ans=max(ans,DP(i));
}
return ans;
}
int main(){
int kase=0;
while(cin>>n&&n){
int ans=solve();
cout<<"Case "<<++kase<<": maximum height = ";
cout<<ans<<endl;
}
return 0;
}
状态设计: dp[i][j]表示以i为底,以j为顶的柱子的最大高度
状态转移: 枚举比i小,比j大的立方体,尝试用dp[i][k]再叠放j立方体的方法更新dp[i][j]。其实很像区间dp,但是这里的区间是有序的。因为dp[i][j]用的是所有已经到达过最底层的状态去更新的,所以dp[i][j]一旦算出就是最大值。
int dp[N][N];//dp[i][j]:以i为底以j为顶的最大值
int DP(int down,int top){
int &ans=dp[down][top];
if(ans)return ans;
for(int i=1;i<=n;i++){
if(i==down)continue;
if(node[i]<node[down]&&node[top]<node[i])
ans=max(ans,DP(down,i)+node[top].z);
}
if(!ans)return node[top].z+node[down].z;
return ans;
}
例3 Tour UVA - 1347
状态设计: dp[i][j]表示第1个人走到第i个点,第2个人走到第j个点时两个人还需要走的距离,且此时1-max(i,j)都被走过了,且设定i>j。
状态转移:
第一个人从i位置走到i+1的位置:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
+
1
]
[
j
]
+
n
o
d
e
[
i
]
.
d
i
s
(
n
o
d
e
[
i
+
1
]
)
,
d
p
[
i
]
[
j
]
)
;
dp[i][j]=min(dp[i+1][j]+node[i].dis(node[i+1]),dp[i][j]);
dp[i][j]=min(dp[i+1][j]+node[i].dis(node[i+1]),dp[i][j]);
第二个人从j位置走到i+1位置:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
+
1
]
[
i
]
+
n
o
d
e
[
j
]
.
d
i
s
(
n
o
d
e
[
i
+
1
]
)
,
d
p
[
i
]
[
j
]
)
;
dp[i][j]=min(dp[i+1][i]+node[j].dis(node[i+1]),dp[i][j]);
dp[i][j]=min(dp[i+1][i]+node[j].dis(node[i+1]),dp[i][j]);
为什么是i+1?
每次都选择一个人走到i+1位置,才能保证从1到i+1每个点都恰好有一个人经过。
第二个状态转移的合理性?
显然
d
p
[
i
]
[
j
]
=
d
p
[
j
]
[
i
]
dp[i][j]=dp[j][i]
dp[i][j]=dp[j][i]。
用定义解释即:"第一个人走到i位置且第二个人走到j位置"的状态 与"第二个人走到i位置且第一个人走到j位置"的状态 其实是同一件事,完全是具有对称性的。回到状态转移方程:”第二个人从j走到i+1,而第一个人留在原地i“ 的状态与 “第二个人(相当于交换位置但不花费距离)去到i,而第一个人去到i+1”的状态 表达的是相同的意思。这么做是为了保持dp第一维总是大于第二维,简化状态转移过程。
枚举顺序: 确定起始状态dp[n-1][n]与dp[n-1][n]后,目标状态为dp[2][1](记住我们已经确定第一维大于第二维)再加上dis(1,2)。第一维的枚举顺序应该从大到小,第二维枚举顺序无关紧要。因为是第一维i+1所有状态更新之后才去更新第一维i的,此时对于所有的j,dp[i+1][j]已经更新完毕。
tips: 枚举顺序由状态转移方程决定,明确已知状态和待求状态。
#include<bits/stdc++.h>
using namespace std;
const int N=1E3+10;
int n;
double dp[N][N];
struct Node{
int x,y;
double dis(const Node &a){return sqrt((a.x-x)*(a.x-x)+(a.y-y)*(a.y-y));}
}node[N];
void solve(){
for(int i=1;i<=n;i++)cin>>node[i].x>>node[i].y;
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dp[i][j]=1e18;
dp[n][n]=0,dp[n-1][n]=dp[n][n-1]=node[n-1].dis(node[n]);
for(int i=n-1;i>=1;i--){
for(int j=i;j>=1;j--){
dp[i][j]=min(dp[i+1][j]+node[i].dis(node[i+1]),dp[i][j]);
dp[i][j]=min(dp[i+1][i]+node[j].dis(node[i+1]),dp[i][j]);
}
}
printf("%.2lf\n",dp[2][1]+node[1].dis(node[2]));
}
int main(){
while(cin>>n)
solve();
return 0;
}
例4 Unidirectional TSP UVA - 116
状态设计: dp[i][j]表示从位置(i,j)出发到达最后1列的最小权值和。起始状态是在最后一列,目标状态是在第一列。
状态转移: 共3个方向,向上或向下或向同行右侧转移。
其他:这道题还需要记录字典序最小的答案,因此每次更新答案时需要记录路径,用一个nxt[i][j]数组记录更新时每个位置可以向哪个方向转移。
#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int N=1E3+10;
int dp[12][110];
int n,m;
int nxt[12][110];
int a[12][110];
void solve(){
memset(dp,0x3f,sizeof dp);
int pos=1,ans=inf;
//dp[i][j]表示到达i行j列位置的最小答案
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>a[i][j];
for(int i=1;i<=n;i++)dp[i][m]=a[i][m];
for(int j=m;j>=1;j--){//从最后1列开始逆推
for(int i=1;i<=n;i++){
int row[3]={i,i-1,i+1};//三个决策方向
if(i==1)row[1]=n;
if(i==n)row[2]=1;
sort(row,row+3);
for(int k=0;k<3;k++){
//记录当前决策的上一个位置,相当于记录路径
if(dp[i][j]>dp[row[k]][j+1]+a[i][j]){
dp[i][j]=dp[row[k]][j+1]+a[i][j];
nxt[i][j]=row[k];//保存行
}
}
if(j==1&&dp[i][j]<ans){
ans=dp[i][j];
pos=i;
}
}
}
cout<<pos;
int j=1;
while(j<m){
cout<<' '<<nxt[pos][j];
pos=nxt[pos][j++];
}cout<<endl;
cout<<ans<<endl;
}
int main(){
while(cin>>n>>m)
solve();
return 0;
}
例5 Jin Ge Jin Qu hao UVA - 12563
总之只要在最后1s唱劲歌金曲就好了。
状态设计: dp[i][j]表示到第i首歌且总的可以唱的时间为j,可以唱的最多歌曲数。
状态转移: 共3个方向,向上或向下或向同行右侧转移。
其他:这道题还需要所能唱的最长的时间,就是记录所有被选中的歌曲。我非常天真地将需要的时间从大到小排了序,恰好过了样例…直觉想的没有任何理论支撑,果不其然,wa了。
//错误代码!!!
#include<bits/stdc++.h>
using namespace std;
const int N=9700;
int kase,n,t;
int dp[60][N];
int a[60];
int vis[60];
bool cmp(int a,int b){return a>b;}
void solve(){
memset(dp,0,sizeof dp);
cin>>n>>t;
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+1+n,cmp);
t--;
int t_need=0,ans=0;
for(int i=1;i<=n;i++){
for(int j=0;j<=t;j++){
dp[i][j]=dp[i-1][j];//不唱当前这首歌
if(j>=a[i]){
if(dp[i-1][j-a[i]]+1>dp[i][j]){
dp[i][j]=max(dp[i][j],dp[i-1][j-a[i]]+1);
if(ans<dp[i][j]){
ans=dp[i][j],t_need=j;
}
}
}
}
}
cout<<"Case "<<++kase<<": "<<dp[n][t]+1<<' '<<t_need+678<<endl;
}
int main(){
int T;cin>>T;
while(T--)
solve();
return 0;
}
参考题解
原来是要用两个dp!(可以用滚动数组优化滚动数组的理解优化)
状态设计: 这题是多决策问题。因为需要两个决策,可以开一个二维数组(如果需要更多的决策可以考虑直接开一个结构体)。dp[i][0]表示可以唱总时长为i时最多可以唱的歌曲数目,dp[i][1]表示可以唱总时长为i时最多可以唱的歌曲时长。
状态转移:分两种情况,当转移后的歌曲数目不变时则考虑时长的转移。
#include<bits/stdc++.h>
using namespace std;
const int N=9700;
int kase,n,t;
int dp[N][2];
int a[60];
int vis[60];
void solve(){
memset(dp,0,sizeof dp);
cin>>n>>t;
for(int i=1;i<=n;i++){
cin>>a[i];
}
t--;
for(int i=1;i<=n;i++){
for(int j=t;j>=a[i];j--){
if(dp[j][0]<dp[j-a[i]][0]+1){
dp[j][0]=dp[j-a[i]][0]+1;
dp[j][1]=dp[j-a[i]][1]+a[i];
}
else if(dp[j][0]==dp[j-a[i]][0]+1)
dp[j][1]=max(dp[j][1],dp[j-a[i]][1]+a[i]);
}
}
cout<<"Case "<<++kase<<": "<<dp[t][0]+1<<' '<<dp[t][1]+678<<endl;
}
int main(){
int T;cin>>T;
while(T--)
solve();
return 0;
}
例6 Another Crisis UVA - 12186
题目链接
到树形dp了!
状态设计:dp[i]表示i向上级递交请愿书需要的工人数量,对于工人而言就是他本身,也就是1。工人的状态是起始状态,都是1,目标状态是老板的dp[0]。
状态转移:求dp[[i],建树之后将i的孩子的dp都求出来,从小到大排序,选前k%求和。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10;
int n,k;
int dp[N];//dp[i]表示让i签字的最少工人数量
vector<int>son[N];
int DP(int u){
int sz=son[u].size();
if(!sz)return dp[u]=1;//工人
if(dp[u])return dp[u];
int cnt=sz*k/100;
if(sz*k%100)cnt++;
vector<int>son_dp;
for(int i=0;i<sz;i++)son_dp.push_back(DP(son[u][i]));
sort(son_dp.begin(),son_dp.end());
for(int i=0;i<cnt;i++)dp[u]+=son_dp[i];
return dp[u];
}
void solve(){
memset(dp,0,sizeof dp);
for(int i=0;i<=n;i++)son[i].clear();
for(int i=1;i<=n;i++){
int x;cin>>x;
son[x].push_back(i);
}
cout<<DP(0)<<endl;
}
int main(){
while(cin>>n>>k&&n&&k)
solve();
return 0;
}