题目链接
洛谷 P1650 田忌赛马
F1:动态规划 1
Task 1
根据题目背景的介绍,我们发现总是田忌针对齐王如何出马而决策,且齐王这些马总是要出的,早出晚出无非就是调换顺序,对答案没有影响。所以我们可以假定齐王按从快到慢的速度出马,并在此基础上进行决策。
又根据题目背景的介绍,我们发现田忌会先用最差的马消耗掉齐王最好的马,所以对于当前状态来说,田忌要么直接出最好的马硬刚,要么用最差的马先消耗掉对方,否则就有点浪费了。所以我们可以先对 \(a_i,b_i\) 进行排序,方便后续处理。
然后,定义 \(dp_{i,j}\) 表示前 \(i\) 场比赛中田忌出了最好的 \(j\) 匹马,最差的 \(i-j\) 匹马,能获得的最大金币数;\(f(x,y)\) 表示田忌的第 \(x\) 匹马与齐王的第 \(y\) 匹马比赛,田忌的金币数的增加值(\(200,0,-200\))。
则对于当前状态,田忌可以是出了当时最好的一匹马,即第 \(j\) 头马后得到的,则 \(dp_{i,j}=dp_{i-1,j-1}+f(j,i)\);或者是出了最差的一匹马,即第 \(n-(i-1-j)=n-i+j+1\) 头马后得到的,则 \(dp_{i,j}=dp{i-1,j}+f(n-i+j+1,i)\)。综上,状态转移方程如下,其中 \(i,j,x,y\) 与定义中含义相同。
代码如下。时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;const int N=2010;
int n;
int a[N],b[N],dp[N][N];inline int f(int x,int y){if (a[x]>b[y]) return 200;else if (a[x]==b[y]) return 0;else return -200;
}
int main(){scanf("%d",&n);for (int i=1;i<=n;++i) scanf("%d",a+i);for (int i=1;i<=n;++i) scanf("%d",b+i);sort(a+1,a+n+1,greater<int>()); // 排序sort(b+1,b+n+1,greater<int>());for (int i=1;i<=n;++i){dp[i][0]=dp[i-1][0]+f(n-i+1,i),dp[i][i]=dp[i-1][i-1]+f(i,i);for (int j=1;j<i;++j) dp[i][j]=max(dp[i-1][j-1]+f(j,i),dp[i-1][j]+f(n-i+j+1,i));}int ans=INT_MIN; // 注意可能一定赔钱for (int i=1;i<=n;++i) ans=max(ans,dp[n][i]);printf("%d",ans);return 0;
}
Task 2
观察到 Task 1 中的代码更新 \(dp_{i,j}\) 时只用到了 \(dp_{i-1,j}\) 与 \(dp_{i-1,j}\),考虑滚动优化。由于更新时要用到的数据在当前格(\(dp_{i-1,j}\))或前一个(\(dp_{i-1,j-1}\)),我们应倒序枚举。即先更新 \(dp_i\),然后倒序更新 \(dp_{[i-1..1]}\),最后更新 \(dp_0\)。
代码如下,时间复杂度不变,空间复杂度 \(O(n)\)。
#include<bits/stdc++.h>
using namespace std;const int N=2010;
int n;
int a[N],b[N],dp[N];inline int f(int x,int y){if (a[x]>b[y]) return 200;else if (a[x]==b[y]) return 0;else return -200;
}
int main(){scanf("%d",&n);for (int i=1;i<=n;++i) scanf("%d",a+i);for (int i=1;i<=n;++i) scanf("%d",b+i);sort(a+1,a+n+1,greater<int>());sort(b+1,b+n+1,greater<int>());for (int i=1;i<=n;++i){dp[i]=dp[i-1]+f(i,i);for (int j=i-1;j>=1;--j) dp[j]=max(dp[j-1]+f(j,i),dp[j]+f(n-i+j+1,i));dp[0]=dp[0]+f(n-i+1,i);}int ans=INT_MIN;for (int i=1;i<=n;++i) ans=max(ans,dp[i]);printf("%d",ans);return 0;
}
F2:区间 DP
考虑区间 DP。首先还是一样,观察到田忌只会出最好的马或最差的马。我们定义 \(dp_{i,j}\) 表示当齐王按从强到弱的顺序出,田忌还剩 \(a_{[i..j]}\) 这些马时,田忌所得的金币最多是多少;\(f(x,y)\) 表示田忌的第 \(x\) 匹马与齐王的第 \(y\) 匹马比赛,田忌的金币数的增加值(\(200,0,-200\))。显然此时要对战的是齐王第 \(n+i-j\) 头马。
所以对于初始化,\(dp_{i,i}\) 指田忌第 \(i\) 匹马对战齐王第 \(n\) 匹马,即 \(f(i,n)\)。然后,对于 \(dp_{i,j}\),它可以由 \(dp_{i+1,j}\) 转移而来,即 \(dp_{i,j}=dp_{i+1,j}+f(i,n+i-j)\);也可以由 \(dp_{i,j-1}\) 转移而来,即 \(dp_{i,j}=dp_{i,j-1}+f(j,n+i-j)\)。所以状态转移方程如下,其中 \(i,j,x,y\) 含义如前文所述。
代码如下,时空复杂度均为 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;const int N=2010;
int n;
int a[N],b[N],dp[N][N];inline int f(int x,int y){if (a[x]>b[y]) return 200;else if (a[x]==b[y]) return 0;else return -200;
}
int main(){scanf("%d",&n);for (int i=1;i<=n;++i) scanf("%d",a+i);for (int i=1;i<=n;++i) scanf("%d",b+i);sort(a+1,a+n+1,greater<int>());sort(b+1,b+n+1,greater<int>());for (int l=1;l<=n;++l){for (int i=1,j=l;j<=n;++i,++j) // 初始化也符合此代码,所以无需单独初始化dp[i][j]=max(dp[i+1][j]+f(i,n+i-j),dp[i][j-1]+f(j,n+i-j));}printf("%d",dp[1][n]);return 0;
}
F3:贪心
首先还是先从大到小排序。假设当前田忌出第 \(i\) 匹马,齐王出第 \(j\) 匹马,则当 \(a_i>b_j\) 时,直接拿下对方。具体情况可见下图枚举。同理,当 \(a_i\le b_j\) 时,我们再比较最弱的马。如果最弱的马能拿下对方最弱的,就先拿分;如果拿不下,就只能让它消耗掉敌方最强的马。具体情况自己画图分析。

所以总结一下,记田忌此时剩余的马为 \(a_{[l..r]}\),齐王此时剩余的马为 \(b_{[p..q]}\)。
- 当 \(a_l>b_p\) 时,\(l\leftarrow l+1,p\leftarrow p+1,ans\leftarrow ans+200\),即田忌用最好马拿下齐王最好马;
- 当 \(a_l\le b_p\) 时:
- 当 \(a_r>b_q\) 时,\(r\leftarrow r-1,q\leftarrow q-1,ans\leftarrow ans+200\),即田忌用最差马拿下齐王最差马;
- 当 \(a_r\le b_q\) 时,\(r\leftarrow r-1,p\leftarrow p+1,ans\leftarrow ans-\begin{cases}0&,a_r=b_p \\200&,a_r<b_p\end{cases}\),即田忌用最差马消耗掉齐王最好马。
代码如下,时间复杂度 \(O(n\log{n})\),瓶颈在排序。
#include<bits/stdc++.h>
using namespace std;const int N=2010;
int n,ans;
int a[N],b[N];int main(){scanf("%d",&n);for (int i=1;i<=n;++i) scanf("%d",a+i);for (int i=1;i<=n;++i) scanf("%d",b+i);sort(a+1,a+n+1,greater<int>());sort(b+1,b+n+1,greater<int>());int l=1,r=n,p=1,q=n;while (n--){if (a[l]>b[p]) ++l,++p,ans+=200;else if (a[r]>b[q]) --r,--q,ans+=200;else ans-=(a[r]==b[p]?0:200),--r,++p; // 注意更新顺序}printf("%d",ans);return 0;
}