LA3983 Robotruck (单调队列优化DP)

时间:2021-02-13 11:13:18

Problem links: https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=1984

题目大意:给出一些垃圾点的坐标x,y(x,y为非负整数)以及每个垃圾的重量,给出机器人的携带量cap,垃圾桶在原点(0,0),机器人从原点出发按顺序拣垃圾,在满足不超过cap的情况下,把垃圾带回原点(回到原点后,携带量重新为cap)。问把所有垃圾拣完需要走的最短路。

一开始想到dp[i][res]表示当前到达i点,当前剩余携带量为res时的最优解。此时,决策有两种,一是把原先携带的先带回原点,即x[i-1]+y[i-1],然后再从原点直接出发到i。另一决策可以是,在满足剩余携带量不小于w[i]时,从i-1走到i。

即状态转移为dp[i][res] = min(x[i-1]+y[i-1]+x[i]+y[i]+dp[i+1][cap-w[i]], x[i]-x[i-1]+y[i]-y[i-1]+dp[i+1][res-w[i]]);复杂度为O(n*cap),其中n<=100000,cap<=100,复杂度很大。但是一开始看错了,以为n<=10000,直接写了个记忆化DP,居然过了。。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

#define SIZE 100010
int cas,cap,n;
int x[SIZE],y[SIZE],w[SIZE];
int dp[SIZE][110];
bool vis[SIZE][110];

int Abs(int x)
{
return x>0?x:-x;
}
int dfs(int cur,int res)
{
if(cur == n+1)
return (x[n]+y[n]);
if(vis[cur][res])
return dp[cur][res];
int tem1 = 0, tem2 = 0;
tem2 += x[cur-1]+y[cur-1];
tem2 += x[cur]+y[cur];
tem2 += dfs(cur+1,cap-w[cur]);
if(res >= w[cur])
{
tem1 += Abs(x[cur]-x[cur-1])+Abs(y[cur]-y[cur-1]);
tem1 += dfs(cur+1,res-w[cur]);
tem2 = min(tem1,tem2);
}
dp[cur][res] = tem2;
vis[cur][res] = true;
return dp[cur][res];
}
int main()
{
scanf("%d",&cas);
while(cas--)
{
scanf("%d%d",&cap,&n);
for(int i=1; i<=n; i++)
scanf("%d%d%d",&x[i],&y[i],&w[i]);
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
dp[1][cap] = x[1]+y[1]+dfs(2,cap-w[1]);
printf("%d\n",dp[1][cap]);
if(cas)
puts("");
}
return 0;
}

正解应该是用单调队列优化,及时删掉肯定不是最优解的点。复杂度降为O(n)

令dp[i]表示拣掉前i个垃圾,并送回原点的最小值。则状态转移方程为:

dp[i] = dp[k] + dis[k+1] + tot[i]-tot[k+1] + dis[i];其中dis[k+1]表示点k+1到原点的直接距离,方程中表示前k个点已经走完,并且机器人在原点,dis[k+1]既是从原点直接走到点k+1,tot[i]表示从点1沿着顺序一直走到点i,即1,2,3...i,故tot[i]-tot[k+1]表示从k+1走到i,最后,再从i走回原点,距离为dis[i]。但是直接枚举k肯定是不行的,这里就用到单调队列来优化。首先,最后结果肯定是dp[n],所以按顺序计算dp[1~n]时,前面肯定是有些子状态是会随着计算后变成没用了。按题意可以知道,当 w[k]+w[k+1]+...w[i] > cap的时候,k就是用不上的了,因为i不可能从k转移而来了。然后,假设有子状态dp[j],dp[k],转移时有dp[i]=dp[j]+dis[j+1]+tot[i]-tot[j+1]+dis[i],以及dp[i] = dp[k]+dis[k+1]+tot[i]-tot[k+1]+dis[i],若dp[i]从dp[j]中转移而来比从dp[k]中转移而来更优(即值更小),则有dp[j]+dis[j+1]+tot[i]-tot[j+1]+dis[i] < dp[k]+dis[k+1]+tot[i]-tot[k+1]+dis[i],化简得dp[j]+dis[j+1]-tot[j+1] < dp[k]+dis[k+1]-tot[k+1],所以可以将最优子子状态存到一个队列里,当当前i不能从队头转移而来时,删掉队头,当决策i比队尾更优时,删掉队尾的那个决策,因为后续状态肯定是从更优的决策i转移而来了。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

#define SIZE 100010
int cas,cap,n;
int x[SIZE],y[SIZE],w[SIZE];
int dp[SIZE],dis[SIZE],tot[SIZE],sumw[SIZE];
int q[SIZE],h,t;

int Abs(int x)
{
return x>0?x:-x;
}

int main()
{
scanf("%d",&cas);
while(cas--)
{
scanf("%d%d",&cap,&n);
x[0] = y[0] = sumw[0] = tot[0] = 0;
for(int i=1; i<=n; i++)
{
scanf("%d%d%d",&x[i],&y[i],&w[i]);
dis[i] = x[i] + y[i];
tot[i] = tot[i-1] + Abs(x[i]-x[i-1]) + Abs(y[i]-y[i-1]);
sumw[i] = sumw[i-1] + w[i];
}
dp[0] = 0;
h = t = 0;
q[++t] = 0;
for(int i=1; i<=n; i++)
{
while(h+2 <= t && sumw[i]-sumw[q[h+1]] > cap)
++h;
dp[i] = dp[q[h+1]] + dis[q[h+1]+1] + dis[i] + tot[i] - tot[q[h+1]+1];
while(h+2 <= t && dp[i]+dis[i+1]-tot[i+1] <= dp[q[t]]+dis[q[t]+1]-tot[q[t]+1])
--t;
q[++t] = i;
}
printf("%d\n",dp[n]);
if(cas)puts("");
}
}