题目描述
有n名同学要乘坐摆渡车从人大附中前往人民大学,第i位同学在第ti分钟去等车。只有一辆摆渡车在工作,但摆渡车容量可以视为无限大。摆渡车从人大附中出发、把车上的同学送到人民大学、再回到人大附中(去接其他同学),这样往返一趟总共花费m分钟(同学上下车时间忽略不计)。摆渡车要将所有同学都送到人民大学。
凯凯很好奇,如果他能任意安排摆渡车出发的时间,那么这些同学的等车时间之和最小为多少呢?
注意:摆渡车回到人大附中后可以即刻出发。
输入格式
第一行包含两个正整数n,m,以一个空格分开,分别代表等车人数和摆渡车往返一趟的时间。
第二行包含n个正整数,相邻两数之间以一个空格分隔,第i个非负整数ti代表第i个同学到达车站的时刻。
输出格式
一行,一个整数,表示所有同学等车时间之和的最小值(单位:分钟)。
输入样例一
5 1
3 4 4 3 5
输出样例一
0
输入样例二
5 5
11 13 1 5 5
输出样例二
4
样例说明
样例一说明:
同学1和同学4在第3分钟开始等车,等待0分钟,在第3分钟乘坐摆渡车出发。摆渡车在第4分钟回到人大附中。
同学2和同学3在第4分钟开始等车,等待0分钟,在第4分钟乘坐摆渡车出发。摆渡车在第5分钟回到人大附中。
同学5在第5分钟开始等车,等待0分钟,在第5分钟乘坐摆渡车出发。自此所有同学都被送到人民大学。总等待时间为 0。
样例二说明:
同学3在第1分钟开始等车,等待0分钟,在第1分钟乘坐摆渡车出发。摆渡车在第6分钟回到人大附中。
同学4和同学5在第5分钟开始等车,等待1分钟,在第6分钟乘坐摆渡车出发。摆渡车在第11分钟回到人大附中。
同学1在第11分钟开始等车,等待2分钟;同学2在第13分钟开始等车,等待0分钟。他/她们在第13分钟乘坐摆渡车出发。自此所有同学都被送到人民大学。
总等待时间为4。可以证明,没有总等待时间小于4的方案。
数据规模
对于10%的数据,n≤10,m=1,0≤ti≤100。
对于30%的数据,n≤20,m≤2,0≤ti≤100。
对于50%的数据,n≤500,m≤100,0≤ti≤10000。
另有20%的数据,n≤500,im≤10,0≤ti≤1000000。
对于100%的数据,n≤500,m≤100,0≤ti≤4000000。
题解
话说当年考场上做这道题时感觉毫无头绪,考完noip后也一直把这题弃着,直到放假了有时间做,发现也就这么一回事。
洛谷上评分是蓝题,个人感觉顶多绿题。。。
回到正题,观察这一题的数据范围,可以看出$n,m$都很小,那我们可以考虑从这方面进行dp。
首先我们按照升序对$a[i]$进行排序(由于个人习惯,这里这里及下文用$a[i]$表示$t[i]$)。
那我们设$dp[i][j]$为第$i$个同学在$a[i]+j$的时间点出发时,第$1$到第$i$个同学的等待时间之和的最小值,可以想到$0 leqslant j < m$。(如果$m leqslant j$的话,那正在等待的同学实际上可以在$a[i]+j-m$的时间点出发,不需要继续等下去,故$0 leqslant j < m$)
容易想到;
$$dp[i][j]=underset{0 leqslant ii < i}{min } {dp[ii][jj]+wait(i, j, ii) }$$
(其中$wait(i, j, ii)$为第$ii$到第$i$个同学的等待时间之和。)
根据上文对$dp[i][j]$的定义,可以得到:$a[i] + j < a[i + 1]$同理$a[ii] + jj < a[ii + 1]$。同时,根据这个方程还可以得到$a[ii] + jj + m leqslant a[i] + j$。
确定好边界之后就好办了。
同时我们也可以想到,如果设$s[i] = sum_{j = 1}^{i} a[j]$,那么有:
$$wait(i, j, ii) = (a[i] + j) imes (i - ii) - (s[i] - s[ii])$$
此时我们可以得到一个时间复杂度为$O(n^{2}m^{2})$的程序。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> #include <cstring> #include <algorithm> #define MAX_N (500 + 5) #define MAX_M (100 + 5) #define INF 0x7f7f7f7f #define WAIT(i, j, ii) ((a[(i)] + j) * ((i) - (ii)) - (s[(i)] - s[(ii)])) using namespace std; int n, m; int a[MAX_N], s[MAX_N]; int dp[MAX_N][MAX_M]; int ans = INF; int main() { cin >> n >> m; for(register int i = 1; i <= n; ++i) { cin >> a[i]; } sort(a + 1, a + n + 1); for(register int i = 1; i <= n; ++i) { s[i] = s[i - 1] + a[i]; } memset(dp, 0x7f, sizeof dp); a[0] = a[1] - m; a[n + 1] = INF; for(register int j = 0; j < m; ++j) { dp[0][j] = 0; } int tmp; for(register int i = 1; i <= n; ++i) { for(register int j = 0; j < m && a[i] + j < a[i + 1]; ++j) { for(register int ii = 0; ii < i; ++ii) { tmp = INF; for(register int jj = 0; jj < m && a[ii] + jj < a[ii + 1] && a[ii] + jj + m <= a[i] + j; ++jj) { tmp = min(tmp, dp[ii][jj]); } if(tmp < INF) dp[i][j] = min(dp[i][j], tmp + WAIT(i, j, ii)); } } } for(register int j = 0; j < m; ++j) { ans = min(ans, dp[n][j]); } cout << ans; return 0; }
观察上面的方程,显然最容易优化的就是求“$min {dp[ii][jj]+wait(i, j, ii) }$"的部分,我们可以想一下是否可以开一个数组维护这些最小值。
事实上,这是可行的。
由于这个方程中,始终有$0 leqslant jj$,那我们何不更改$dp[i][j]$的定义为:第$i$个同学在$left [ a[i], a[i] + j ight ]$的时间段里出发时,第$1$到第$i$个同学的等待时间之和的最小值。
这样,我们只需要每次更新完当前的$dp[i][j]$时,和$dp[i][j-1]$取一个最小值就行了,这样我们就可以把时间复杂度降到$O(n^{2}m)$,具体的细节可以参考下面给出的程序。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> #include <cstring> #include <algorithm> #define MAX_N (500 + 5) #define MAX_M (100 + 5) #define INF 0x7f7f7f7f #define WAIT(i, j, ii) ((a[(i)] + j) * ((i) - (ii)) - (s[(i)] - s[(ii)])) using namespace std; int n, m; int a[MAX_N], s[MAX_N]; int dp[MAX_N][MAX_M]; int ans = INF; int main() { cin >> n >> m; for(register int i = 1; i <= n; ++i) { cin >> a[i]; } sort(a + 1, a + n + 1); for(register int i = 1; i <= n; ++i) { s[i] = s[i - 1] + a[i]; } memset(dp, 0x7f, sizeof dp); a[0] = a[1] - m; a[n + 1] = INF; for(register int j = 0; j < m; ++j) { dp[0][j] = 0; } int tmp; for(register int i = 1; i <= n; ++i) { for(register int j = 0; j < m; ++j) { if(a[i] + j < a[i + 1]) { for(register int ii = 0; ii < i; ++ii) { tmp = min(m - 1, min(a[ii + 1] - a[ii] - 1, (a[i] + j) - (a[ii] + m))); if(tmp < 0) continue; if(dp[ii][tmp] < INF) dp[i][j] = min(dp[i][j], dp[ii][tmp] + WAIT(i, j, ii)); } } dp[i][j] = min(dp[i][j], dp[i][j - 1]); } } cout << dp[n][m - 1]; return 0; }
实际上这样做还是麻烦了。
我们可以想,第$i$个人要么和第$i - 1$个人同车,要么和第$i - 1$个人不同车,要在其中取最优解。也就是说,第$i$个人只和第$i - 1$个人有关,也就是说$ii$其实只需要取$i - 1$。
让$dp[i][j]$回到原来的定义。容易得到,当第$i$个人和第$i - 1$个人同车时,有状态转移方程:
$$dp[i][j] = dp[i - 1][a[i] + j - a[i - 1]] + j$$
而当第$i$个人和第$i - 1$个人不同车时,容易得到:
$$dp[i][j] = underset {0 leqslant k leqslant a[i] + j - a[j] - m, 0 leqslant k < m imes 2} {min} { dp[i - 1][k] } + j$$
像前面一样维护最小值即可。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> #include <algorithm> #include <cstring> #define MAX_N (500 + 5) #define MAX_M (100 + 5) using namespace std; int n, m; int a[MAX_N]; int dp[MAX_N][MAX_M << 1], mdp[MAX_N][MAX_M << 1]; int main() { cin >> n >> m; for(register int i = 1; i <= n; ++i) { cin >> a[i]; } sort(a + 1, a + n + 1); memset(dp, 0x7f, sizeof dp); memset(mdp, 0x7f, sizeof mdp); for(register int i = 0; i < (m << 1); ++i) { dp[1][i] = i; mdp[1][i] = 0; } for(register int i = 2; i <= n; ++i) { for(register int j = 0, k1 = a[i] - a[i - 1] - m, k2 = a[i] - a[i - 1]; j < (m << 1); ++j, ++k1, ++k2) { if(k1 >= 0) dp[i][j] = min(dp[i][j], mdp[i - 1][min(k1, (m << 1) - 1)] + j); if(k2 >= 0 && k2 < (m << 1)) dp[i][j] = min(dp[i][j], dp[i - 1][k2] + j); } mdp[i][0] = dp[i][0]; for(register int j = 1; j < (m << 1); ++j) { mdp[i][j] = min(dp[i][j], mdp[i][j - 1]); } } cout << mdp[n][(m << 1) - 1]; return 0; }