P5017/Noip2018-PJT3 摆渡车 题解
update 2021/1/19:
- 原题解部分 Markdown 和 Latex 炸了,现在已经修复。
- 对题解当中一些表述不明的句子做了进一步的阐述。
- 更换了码风。
首先,看到这道题在普及组 T3 的位置,我们知道只能是 搜索/图论/DP 三者之一。
再结合数据范围我们就可以 根据个人经验 分析出这道题是 DP 题。
但或许是我太弱了,没有想出 DP 怎么写,最后用了记忆化搜索。
在写记忆化搜索时,我的方法是: 先写爆搜,通过小样例之后再加记忆化。
对于本题而言,我最开始想到的是从第 \(n\) 个人倒回去处理,设他在第 \(x\) 分钟离开( \(t_i \leq x\)),进行搜索。 \(dfs(k,g)\) 表示第 \(k\) 个人在时间点 \(g\) 之后离开,枚举第 \(k-1\) 离开时间点后判断上一个人是否会与这个人同车。
但是这样显然时间复杂度与 \(O(nt)\) 有关,妥妥的 TLE。
到这里,我们会发现有两条路:往 \(O(nm)\) 走或者往 \(O(t)\) 走。
接下来,我分析了题目,发现 每一个人至多等待 \(2 \times m\) 分钟 ,因为对于第 \(i\) 个人,如果 \(x-t_i > 2m\) ,表明此时他在的时候车子已经开过一遍了,而他却还是呆在原地,显然不是最优的选择。
所以,我们没有必要去枚举 \(x\) ,只需要枚举一个数 \(k\) ( \(0 \leq k \leq 2m\) ,即为等车时间),则第 \(i\) 个人发车时间为 \(t_i+k\) 。这样就避免了因为 \(t\) 的范围而引起的超时,方向为 \(O(nm)\)。
那么接下来如何判断第 \(i-1\) 个人与第 \(i\) 个人是否同车呢?
同样,我们枚举 \(k'\) ,表示第 \(i-1\) 个人的等车时间。
- 如果 \(t_{i-1}+k'=t_i+k\) ,表明此时两个人发车时间相同,那么一起走。
- 如果 \(t_{i-1}+k'+m \leq t_i+k\) ,表明此时第 \(i-1\) 个人坐车到达,车子回到人大附中之后第 \(i\) 个人还没有发车,那么第 \(i-1\) 个人直接发车离开。
- 如果 \(t_{i-1}+k'+m > t_i+k\) ,表明第 \(i-1\) 个人发车走后第 \(i\) 个人还要再等,但是这种情况按照上面所述显然不优:
- 如果第 \(i-1\) 个人走了之后第 \(i\) 个人都还没有到,那么我们完全不需要去理第 \(i\) 个人,那么直接发车走人,而且此时肯定是越早走越好,那么我们在枚举 \(k'\) 的时候已经解决了。
- 否则,说明第 \(i-1\) 个人跟第 \(i\) 个人在一起等车,但是第 \(i-1\) 个人走了第 \(i\) 个人还呆在那里,之前已经解释过肯定不是最优解,那么也不需要去管。
分析完毕,此时 \(dfs(k,g)\) 表示第 \(k\) 个人在 第 \(t_k+g\) 的时间发车(而不是第 \(g\) 时间点发车) 时的最小值。函数中,我们枚举 \(i\) (\(0 \leq i < 2m\)),进行上述判断,如果满足任意条件,更新答案。
更新答案时,我的方式是函数中令 \(sum= INF\) ,满足任一条件时, \(sum \gets min(sum,dfs(k-1,i)+g)\)( \(dfs(k-1,i)+g\) 表示第 \(k-1\) 个人在 \(t_{k-1}+g\) 时发车时等待时间最小值,加上第 \(k\) 个人等待时间 \(g\) ),最后返回 \(sum\) 即可。
不要忘记对 \(t\) 排序!
暴力代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 500 + 10;
int n, m, t[MAXN], f[MAXN][MAXN], ans;//f[i][j] 表示第 i 个人在等了 j 时间后发车的最优解
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
int Min(int a, int b) {return (a < b) ? a : b;}
int dfs(int k, int g)
{
if (k == 1) return g;
int ans = 0x7f7f7f7f;
for (int i = 0; i < (m << 1); ++i)
{
if (t[k - 1] + i == t[k] + g) ans = Min(ans, dfs(k - 1, i) + g);
if (t[k - 1] + i + m <= t[k] + g) ans = Min(ans, dfs(k - 1, i) + g);
}
return ans;
}//暴力dfs
int main()
{
n = read(), m = read(), ans = 0x7f7f7f7f;
for (int i = 1; i <= n; ++i) t[i] = read();
sort(t + 1, t + n + 1); memset(f, -1, sizeof(f));
for (int i = 0; i< (m << 1); ++i) ans = Min(ans, dfs(n, i));
printf("%d\n", ans); return 0;
}
接下来加记忆化。其实在写记忆化搜索的时候,我个人认为加记忆化是最简单的, 只要爆搜写好并且写对,加记忆化易如反掌。哪里有 return ,哪里加记忆化 。最后代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 500 + 10;
int n, m, t[MAXN], f[MAXN][MAXN], ans;//f[i][j] 表示第 i 个人在等了 j 时间后发车的最优解,初始化为 -1。
int read()
{
int sum = 0, fh = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') fh = -1; ch = getchar();}
while (ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
return sum * fh;
}
int Min(int a, int b) {return (a < b) ? a : b;}
int dfs(int k, int g)
{
if (f[k][g] != -1) return f[k][g];
if (k == 1) return f[k][g] = g;
int ans = 0x7f7f7f7f;
for (int i = 0; i < (m << 1); ++i)
{
if (t[k - 1] + i == t[k] + g) ans = Min(ans, dfs(k - 1, i) + g);
if (t[k - 1] + i + m <= t[k] + g) ans = Min(ans, dfs(k - 1, i) + g);
}
return f[k][g] = ans;
}
int main()
{
n = read(), m = read(), ans = 0x7f7f7f7f;
for (int i = 1; i <= n; ++i) t[i] = read();
sort(t + 1, t + n + 1); memset(f, -1, sizeof(f));
for (int i = 0; i< (m << 1); ++i) ans = Min(ans, dfs(n, i));
printf("%d\n", ans); return 0;
}
最后的总结:其实对于普及的 DP ,如果没有好思路时,可以尝试写记忆化搜索,或许就能解出 DP 题而成功的获得高分。