开车旅行问题

小 A 和小B 决定利用假期外出旅行,他们将想去的城市从 11 到 nn 编号,且编号较小的城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市 ii 的海拔高度为h_ihi​,城市 ii 和城市 jj 之间的距离 d_{i,j}di,j​ 恰好是这两个城市海拔高度之差的绝对值,即 d_{i,j}=|h_i-h_j|di,j​=∣hi​−hj​∣。

旅行过程中,小 \text{A}A 和小 \text{B}B 轮流开车,第一天小 \text{A}A 开车,之后每天轮换一次。他们计划选择一个城市 ss 作为起点,一直向东行驶,并且最多行驶 xx 公里就结束旅行。

小 \text{A}A 和小 \text{B}B 的驾驶风格不同,小 \text{B}B 总是沿着前进方向选择一个最近的城市作为目的地,而小 \text{A}A 总是沿着前进方向选择第二近的城市作为目的地(注意:本题中如果当前城市到两个城市的距离相同,则认为离海拔低的那个城市更近)。如果其中任何一人无法按照自己的原则选择目的城市,或者到达目的地会使行驶的总距离超出 xx 公里,他们就会结束旅行。

在启程之前,小 A 想知道两个问题:

1、 对于一个给定的 x=x_0x=x0​,从哪一个城市出发,小 \text{A}A 开车行驶的路程总数与小 \text{B}B 行驶的路程总数的比值最小(如果小 \text{B}B 的行驶路程为 00,此时的比值可视为无穷大,且两个无穷大视为相等)。如果从多个城市出发,小 \text{A}A 开车行驶的路程总数与小 \text{B}B 行驶的路程总数的比值都最小,则输出海拔最高的那个城市。

2、对任意给定的 x=x_ix=xi​ 和出发城市 s_isi​,小 \text{A}A 开车行驶的路程总数以及小 \text BB 行驶的路程总数。

各个城市的海拔高度以及两个城市间的距离如上图所示。

如果从城市 11 出发,可以到达的城市为 2,3,42,3,4,这几个城市与城市 11 的距离分别为 1,1,21,1,2,但是由于城市 33 的海拔高度低于城市 22,所以我们认为城市 33 离城市 11 最近,城市 22 离城市 11 第二近,所以小A会走到城市 22。到达城市 22 后,前面可以到达的城市为 3,43,4,这两个城市与城市 22 的距离分别为 2,12,1,所以城市 44 离城市 22 最近,因此小B会走到城市44。到达城市 44 后,前面已没有可到达的城市,所以旅行结束。

如果从城市 22 出发,可以到达的城市为 3,43,4,这两个城市与城市 22 的距离分别为 2,12,1,由于城市 33 离城市 22 第二近,所以小 \text AA 会走到城市 33。到达城市 33 后,前面尚未旅行的城市为 44,所以城市 44 离城市 33 最近,但是如果要到达城市 44,则总路程为 2+3=5>32+3=5>3,所以小 \text BB 会直接在城市 33 结束旅行。

如果从城市 33 出发,可以到达的城市为 44,由于没有离城市 33 第二近的城市,因此旅行还未开始就结束了。

如果从城市 44 出发,没有可以到达的城市,因此旅行还未开始就结束了。

首先预处理出从每个城市出发,小A和小B开车分别会到达的下一个城市。分析题意后很容易知道,对于当前编号为xx的城市,所求的就是编号x+1x+1~nn的城市构成的集合中距离城市xx最近和次近的城市。这个问题可以用平衡树或双向链表求解。由于本人太菜,不会写双向链表,所以在这里用STL的set实现,想要了解双向链表实现的大佬们请看别的题解。

如何用平衡树实现?显然,在由编号xx~nn的城市构成的平衡树上,离城市xx最近的一定是xx的前驱或后继,次近的一定在xx的前驱、后继、前驱的前驱和后继的后继当中。我们只要取出这些节点,分别比较与xx的距离,找到最小的两个即可。

在这里有几点要注意的:

其中k=0k=0表示小A先开车,k=1k=1表示小B先开车。

转移方程还是比较好想的,在DP时,ii作为阶段,jj作为状态,对于每个天数2^i2i,分别从前后两半时间转移过来即可。在转移时要注意,因为2^0=120=1是奇数,因此在转移到2^121时,前后两半时间先开车的人不一样,因此要独立出来转移;其余的状态转移方程均相同。

DP初值:

其中dist_{x,y}distx,y​表示城市xx到城市yy的距离

以上的初值设置很好理解,请读者结合数组的定义自己思考。

初值的设置为了方便可以直接在上面的预处理过程中进行。

DP初值赋值代码:

int f[20][N][5],da[20][N][5],db[20][N][5];  
  
for(int i=n;i;i--)
	{
		...//预处理
		f[0][i][0]=ga,f[0][i][1]=gb;
		da[0][i][0]=abs(h[i]-h[ga]);
		db[0][i][1]=abs(h[i]-h[gb]);
	}

状态转移方程:

当i=1i=1时:

当i>1i>1时:

具体原理请读者自己思考,下面的代码中也会进行讲解。

DP转移代码:

for(int i=1;i<=18;i++)
		for(int j=1;j<=n;j++)
			for(int k=0;k<2;k++)
				if(i==1)//此时后半段先开车的人和整段先开车的人不同
				{
					f[1][j][k]=f[0][f[0][j][k]][1-k];//整段的路程即后半段到达的路程,后半段的起点即前半段的终点
					da[1][j][k]=da[0][j][k]+da[0][f[0][j][k]][1-k];//整段小A行驶的路程即前半段和后半段小A行驶的路程之和
					db[1][j][k]=db[0][j][k]+db[0][f[0][j][k]][1-k];//整段小B行驶的路程即前半段和后半段小B行驶的路程之和	
				}
				else//此时后半段先开车的人和整段先开车的人相同,其余与上面一样,就不再赘述
				{
					f[i][j][k]=f[i-1][f[i-1][j][k]][k];
					da[i][j][k]=da[i-1][j][k]+da[i-1][f[i-1][j][k]][k];
					db[i][j][k]=db[i-1][j][k]+db[i-1][f[i-1][j][k]][k];
				}

求解问题1

很显然,要知道小A和小B行驶路程的比值,只要知道他们分别行驶的路程就可以了。这里定义一个函数calc(S,X)calc(S,X)求解从城市SS出发,最多行驶XX公里,小A和小B分别行驶了多少距离。

通过上面DP推出的三个数组,calccalc函数的实现就不难了,只要倍增模拟前进即可。

具体实现:

calccalc函数解决之后,求解问题1就非常简单了。只要枚举每个城市i=1i=1~nn,计算出calc(i,x0)calc(i,x0),比较出la/lbla/lb最小且海拔最高的那个城市,即为答案。

求解问题1代码:

int la,lb,ansid;
  
void calc(int S,int X)
{
	int p=S;
	la=0,lb=0;//初始化
	for(int i=18;i>=0;i--)
		if(f[i][p][0] && la+lb+da[i][p][0]+db[i][p][0]<=X)
		{
			la+=da[i][p][0];
			lb+=db[i][p][0];
			p=f[i][p][0];
		}//倍增模拟前进
}
                                                          
for(int i=1;i<=n;i++)
{
	calc(i,x0);
	double nowans=(double)la/(double)lb;//注意精度问题
	if(nowans<ans)
	{
		ans=nowans;
		ansid=i;
	}
	else
		if(nowans==ans && h[ansid]<h[i])
			ansid=i;//比较求解
}
cout<<ansid<<endl;
  • 由于查找时会涉及到xx周围的四个节点,因此在一开始要先向平衡树分别插入2个极大值和极小值节点,以免越界。
  • 由于在平衡树中每个城市需要保存编号和海拔两个数据,因此在使用set时要重载运算符来保证节点set中的顺序。
  • set的迭代器只支持++和--两个操作,不能使用其它运算。

    倍增优化DP

    本题中DP需要用到三个数组:

  • f_{i,j,k}fi,j,k​表示从城市jj出发,行驶2^i2i天,kk先开车,最终会到达的城市
  • da_{i,j,k}dai,j,k​表示从城市jj出发,行驶2^i2i天,kk先开车,小A行驶的路程长度
  • db_{i,j,k}dbi,j,k​表示从城市jj出发,行驶2^i2i天,kk先开车,小B行驶的路程长度
  • f_{0,j,0}=ga_j,f_{0,j,1}=gb_jf0,j,0​=gaj​,f0,j,1​=gbj​
  • da_{0,j,0}=dist_{j,ga_j},da_{0,j,1}=0da0,j,0​=distj,gaj​​,da0,j,1​=0
  • db_{0,j,0}=0,db_{0,j,1}=dist_{j,gb_j}db0,j,0​=0,db0,j,1​=distj,gbj​​
  • f_{1,j,k}=f_{0,f_{0,j,k},1-k}f1,j,k​=f0,f0,j,k​,1−k​
  • da_{1,j,k}=da_{0,j,k}+da_{0,f_{0,j,k},1-k}da1,j,k​=da0,j,k​+da0,f0,j,k​,1−k​
  • db_{1,j,k}=db_{0,j,k}+db_{0,f_{0,j,k},1-k}db1,j,k​=db0,j,k​+db0,f0,j,k​,1−k​
  • f_{i,j,k}=f_{i-1,f_{i-1,j,k},k}fi,j,k​=fi−1,fi−1,j,k​,k​
  • da_{i,j,k}=da_{i-1,j,k}+da_{i-1,f_{i-1,j,k},k}dai,j,k​=dai−1,j,k​+dai−1,fi−1,j,k​,k​
  • db_{i,j,k}=db_{i-1,j,k}+db_{i-1,f_{i-1,j,k},k}dbi,j,k​=dbi−1,j,k​+dbi−1,fi−1,j,k​,k​
  • 初始化p=S,la=0,lb=0p=S,la=0,lb=0
  • 倒序循环i=\log ni=logn~00,对于每个ii,若从当前的pp行驶2^i2i天仍不超过XX,则前进;即若la+lb+da_{i,p,0}+db_{i,p,0}\leq Xla+lb+dai,p,0​+dbi,p,0​≤X,则令la+=da_{i,p,0},lb+=db_{i,p,0},p=f_{i,p,0}la+=dai,p,0​,lb+=dbi,p,0​,p=fi,p,0​。判断时还要注意一个条件,因为要保证不超过最后一个城市,即不越界,因此要特判当前枚举的终点在nn个城市之内,其实只要f_{i,p,0}>0fi,p,0​>0就满足条件。
  • 第2步结束后,lala和lblb就是所求的答案,分别表示小A和小B行驶的路程。
  • struct City
    {
    	int id,al;//identifier编号,altitude海拔
    	friend bool operator < (City a,City b)
        {
            return a.al<b.al; 
        }//重载运算符,按照海拔升序
    };//存储城市信息
    
    multiset<City> q;//支持集合内重复元素的set
    
    h[0]=INF,h[n+1]=-INF;
    City st;//start
    st.id=0,st.al=INF;
    q.insert(st),q.insert(st);
    st.id=n+1,st.al=-INF;
    q.insert(st),q.insert(st);//插入4个初始节点
    for(int i=n;i;i--)//倒序插入,保证set内只有编号x~n的城市
    {
    	int ga,gb;//分别表示从当前城市出发小A和小B开车的下一站
    	City now;
    	now.id=i,now.al=h[i];
    	q.insert(now);//插入当前城市
    	set<City>::iterator p=q.lower_bound(now);//迭代器,在这里指向now节点
    	p--;
    	int lt=(*p).id,lh=(*p).al;//last前驱
    	p++,p++;
    	int ne=(*p).id,nh=(*p).al;//next后继
    	p--;
    	if(abs(nh-h[i])>=abs(h[i]-lh))//若前驱更近
    	{
    		gb=lt;
    		p--,p--;//找到前驱的前驱
    		if(abs(nh-h[i])>=abs(h[i]-(*p).al))//若前驱的前驱更近
    			ga=(*p).id;
    		else//若后继更近
    			ga=ne;
    	}
    	else//若后继更近
    	{
    		gb=ne;
    		p++,p++;//找到后继的后继
    		if(abs((*p).al-h[i])>=abs(h[i]-lh))//若前驱更近
    			ga=lt;
    		else//若后继的后继更近
    			ga=(*p).id;
    	}
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值