CCF 201809-4 再卖菜

问题描述

  在一条街上有n个卖菜的商店,按1至n的顺序排成一排,这些商店都卖一种蔬菜。
  第一天,每个商店都自己定了一个正整数的价格。店主们希望自己的菜价和其他商店的一致,第二天,每一家商店都会根据他自己和相邻商店的价格调整自己的价格。具体的,每家商店都会将第二天的菜价设置为自己和相邻商店第一天菜价的平均值(用去尾法取整)。
  注意,编号为1的商店只有一个相邻的商店2,编号为n的商店只有一个相邻的商店n-1,其他编号为i的商店有两个相邻的商店i-1和i+1。
  给定第二天各个商店的菜价,可能存在不同的符合要求的第一天的菜价,请找到符合要求的第一天菜价中字典序最小的一种。
  字典序大小的定义:对于两个不同的价格序列(a1, a2, ..., an)和(b1, b2, b3, ..., bn),若存在i (i>=1), 使得ai<bi,且对于所有j<i,aj=bj,则认为第一个序列的字典序小于第二个序列。

输入格式

  输入的第一行包含一个整数n,表示商店的数量。
  第二行包含n个正整数,依次表示每个商店第二天的菜价。

输出格式

  输出一行,包含n个正整数,依次表示每个商店第一天的菜价。

样例输入

8
2 2 1 3 4 9 10 13

样例输出

2 2 2 1 6 5 16 10

数据规模和约定

  对于30%的评测用例,2<=n<=5,第二天每个商店的菜价为不超过10的正整数;
  对于60%的评测用例,2<=n<=20,第二天每个商店的菜价为不超过100的正整数;
  对于所有评测用例,2<=n<=300,第二天每个商店的菜价为不超过100的正整数。
  请注意,以上都是给的第二天菜价的范围,第一天菜价可能会超过此范围。

分析:

本题考的时候没时间了就没得分,现在重做看到题第一印象就是递推。如果这里平均数没有用去尾法取整的话,是个较为简单的递推题,二十行以内代码就可以解决。很快写完递推后发现不是简单的递推题,而是差分约束。我是用dfs+记忆化搜索强行ac的,并未使用差分约束的相关知识。如果后面用差分约束求解了,我再进行补充。就dfs而言,本题是个相当好的练习dfs的题目。不论是对dfs程序的编写,注意事项,还是对记忆化搜索理解的提高,都有重要的帮助。

解题思路:

本题的递推式为:a[1] =( b[1] + b[2]) / 2,a[n] =( b[n] + b[n-1]) / 2,(首尾边界情况),其中a[n]表示第二天菜价,b[n]表示第一天菜价。一般情况的递推式:a[k] = (b[k-1] + b[k] + b[k+1]) / 2。当然,这题是让我们根据第二天的推导第一天的菜价。对第一天而言,菜价可以是1到2*a[1],为什么可以取到2*a[1]呢,是因为比如a[1] = 12,那么b[1] = 24,b[2] = 1,也是没有问题的。同样对于b[2]而言,可以是k = 2*a[1] - b[1],也可以是k + 1,同样的式子可类比到b[n],对于一般情况,我们知道了b[1],b[2],怎么得到b[3]呢,由于a[2] = (b[1] + b[2] + b[3]) / 3,那么令t = 3 * a[2] - b[1] - b[2] , b[3]可以取t到t+2之间所有的值。(原因:比如x / 3 = 2,则x可以是6,7,8都满足该式,也就是x / 3 = y,x可以取3*y,3*y+1,3*y+2)。

因为边界情况和一般情况递推式有点区别,我们可以先迭代的形式写出b[1],b[2],也就是

for(int i = 1;i < 2 * a[1] +1;i++){
		b[1] = i;
		int k = 2*a[1]-b[1];
		for(int j = k;j < k + 2;j++){
			if(j <= 0)	continue;
			b[2] = j;
			dfs(3);
		}
	}

然后从第三项开始dfs,注意,这里的菜价都是正整数,这也是重要的边界条件。

dfs如何去写?

cur表示递归到第几个摊位的菜价 ,当然必不可少,边界情况呢?遍历完了n个摊位,也就是cur增加到n+1时,就是边界条件了。注意我们按照一般的递推式推导b[n]时,是先t = 3 * a[n-1] - b[n-1] - b[n-2],然后依次枚举t到t+2,找到b[n]为正整数的情况。当然这里容易忽略的是b[n]不仅要满足上面的式子,还要满足边界时a[n] = (b[n-1] + b[n] / 2)。所以边界还要加上if(a[n] != (b[n - 1] + b[n]) / 2)    return;这里dfs是按照字典序遍历的,所以第一个满足边界的解就是字典序最小的最优解,我们输出它后为了节省时间,避免递归返回可以直接exit(0),结束程序。dfs主体怎么写?比较简单,直接看下面代码就懂了。

版本一:DFS(30分)

#include <iostream>
using namespace std;
const int maxn = 305;
int n,t,a[maxn],b[maxn];
void dfs(int cur){
	if(cur == n + 1){
		if(a[n] != (b[n - 1] + b[n]) / 2)	return;
		for(int j = 1;j <= n;j++)	cout<<b[j]<<" ";
		exit(0);
	}
	t = 3 * a[cur - 1] - b[cur - 1] - b[cur - 2];
	for(int i = t;i < t + 3;i++){
		if(i <= 0)	continue;
		b[cur] = i;
		dfs(cur+1);
	}
}
int main(){
	cin>>n;
	for(int i = 1;i <= n;i++)	cin>>a[i];
	for(int i = 1;i < 2 * a[1] +1;i++){
		b[1] = i;
		int k = 2*a[1]-b[1];
		for(int j = k;j < k + 2;j++){
			if(j <= 0)	continue;
			b[2] = j;
			dfs(3);
		}
	}
	return 0;
}

可以仔细观察上面的代码,发现每一步的逻辑都无可挑剔,可以过题目所给的样例,提交只有三十分也不是超时的缘故,而是存在漏洞。为了找到问题,我写了对拍程序,生成了随机数,与别人的标程运行结果进行对比,测试用例如下:

输入:

10
34 23 31 31 38 54 45 38 31 28 

正确输出是1 67 1 25 67 22 74 39 1 55,而执行上面程序的输出是1 67 1 26 66 22 74 39 1 55。仔细检查代码逻辑还是找不到问题,于是我按照标程输出来调试,发现上面代码执行到第七个摊位时,执行到73,但并不会执行完回溯继续执行74,还是找不到问题,最后发现了是全局变量的锅,注意上面代码我将t定义为全局变量,然后dfs时栈并不会保存全局变量,所以一旦某次调用更改了t的值,回溯时你就找不到原来的值了,从而导致错误。于是,将全局变量t改为局部变量。

版本二:DFS(80分)

#include <iostream>
using namespace std;
const int maxn = 305;
int n,a[maxn],b[maxn];
void dfs(int cur){
	if(cur == n + 1){
		if(a[n] != (b[n - 1] + b[n]) / 2)	return;
		for(int j = 1;j <= n;j++)	cout<<b[j]<<" ";
		exit(0);
	}
	int t = 3 * a[cur - 1] - b[cur - 1] - b[cur - 2];
	for(int i = t;i < t + 3;i++){
		if(i <= 0)	continue;
		b[cur] = i;
		dfs(cur+1);
	}
}
int main(){
	cin>>n;
	for(int i = 1;i <= n;i++)	cin>>a[i];
	for(int i = 1;i < 2 * a[1] +1;i++){
		b[1] = i;
		int k = 2*a[1]-b[1];
		for(int j = k;j < k + 2;j++){
			if(j <= 0)	continue;
			b[2] = j;
			dfs(3);
		}
	}
	return 0;
}

不仔细看根本发现不了改动,就是改动了t的定义,提交就变成了80分了。返回的是超时错误,当然也无可厚非,dfs本来就是消耗大量时间。从上面对dfs的细微修改导致结果巨大的不同可以发现:在写dfs时,每一个变量是全局的还是局部的都要慎重考虑,全局变量的重用要考虑恢复状态,这里t定义为全局变量导致出错就相当于一些题中保存状态的全局数组没有恢复状态 ,dfs里面出现的变量,不论是形参,还是函数内定义的变量,数组,都不可小视。

下面来解决超时问题,无非两种思路,剪枝或者记忆化搜索,剪枝的话上面代码已经足够简练,多余的遍历很难再去削减,于是考虑用记忆化搜索去剪枝。

dfs耗时的主要原因是重复计算,也就是解答树上的子结点被重复计算,解决它的办法就是存储中间多次会用到的状态,这样的做法有两种思路,迭代实现的叫做动态规划,递归实现的叫做记忆化搜索。简单的递归程序比如阶乘或者斐波那契数列都可以使用记忆化搜索或者改成尾递归将中间结果保存在形参里来消除重复计算。

这题的重复状态不容易发现,我们分析主要的递推式t = 3 * a[cur - 1] - b[cur - 1] - b[cur - 2],发现,b[cur]虽然不是严格的等于t,但是一旦t确定了,b[cur]的三种取值也就确定了。例如:1 3  4*****和2 2 5*****,序列尽管不同,但是既然前面遍历的序列1 3  4*****没有使得程序终止,说明该方案行不通,而对于此时的t,3+4=2+5,前面两种序列对后面状态的影响完全一样,某时刻cur取特定的值,b[cur]取特定的值,b[cur-1]取特定的值,不论之前其他的菜价如何,我们根据以及确定的cur,b[cur],b[cur-1]推出的后面序列完全一样,(这里a数组是不变的),所以,我们第一次遍历到这种状态{cur,b[cur],b[cur-1]}发现往后遍历得不到解,那么下次再遍历到这种状态便同样不会得到解,换句话说,b[cur+1]只与b[cur],b[cur-1]有关,与b[1]到b[cur-2]都无关。

通过上面的分析,我们可以定义一个全局数组vis[maxn][maxn][maxn],第一维表示cur,第二维表示b[cur],第三维表示b[cur-1],在遍历cur+1前先根据vis数组判断这种状态是否已经被遍历过,没有被遍历过,则vis置一,遍历cur+1;否则则考虑下一种状态,不再重复遍历。具体代码如下:

版本三:DFS + 记忆化搜索(100分)

#include <iostream>
using namespace std;
const int maxn = 305;
int n,a[maxn],b[maxn];
bool vis[maxn][maxn][maxn];//第一维-第n天;第二维-第n天的值;第三维-第n-1天值 
void dfs(int cur){
	if(cur == n + 1){
		if(a[n] != (b[n - 1] + b[n]) / 2)	return;
		for(int j = 1;j <= n;j++)	cout<<b[j]<<" ";
		exit(0);
	}
	int t = 3 * a[cur - 1] - b[cur - 1] - b[cur - 2];
	for(int i = t;i < t + 3;i++){
		if(i <= 0)	continue;
		b[cur] = i;
		if(vis[cur][i][b[cur-1]])	continue; 
		vis[cur][i][b[cur-1]] = 1;
		dfs(cur+1);
	}
}
int main(){
	cin>>n;
	for(int i = 1;i <= n;i++)	cin>>a[i];
	for(int i = 1;i < 2 * a[1] +1;i++){
		b[1] = i;
		int k = 2*a[1]-b[1];
		for(int j = k;j < k + 2;j++){
			if(j <= 0)	continue;
			b[2] = j;
			dfs(3);
		}
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_30277239/article/details/87892339