【PAT-T】1017. The Best Peak Shape

时间:2021-10-19 18:44:34

首先对题目进行分解。

 

找峰值The Best Peak Shape,可以理解成一个峰值点,或者也可以理解成截断点,和两条单调的子序列。

 

首先很容易就能想到,从前往后求上升A[i],从后往前上升的B[i],再对所有求得的值进行遍历,可以找到满足:A[i]+B[i]最大的情况下,|A[i]-B[i]|最小。

 

那么我们先对子序列进行分析。

我们可以看到这是一个LIS问题(Longest Increasing Subsequence),也就是说找到最长上升子序列.

 

那么在动态规划的学习以前,我们尝试用已有的知识尝试对这个问题进行解决。



方法一 有向图可以使用BST或者DST进行搜索

 

我们发现寻找这个最长子序列的过程可以理解成一个有向图里的搜索问题。

-INF->1->3->4->6->2->5->+INF

1->2 1->5 1->6

3->6 3->4

相当于是求从原点到最终点的最长路径

那么结合我们已有的知识,我们可以使用DFS或者BFS进行搜索。

但在这里会超时过不了测试点

复杂度分析比较困难,在最坏情况下,比如在一个递增的序列中求最长子序列,他的复杂度会达到2^n

#include<iostream>
#include<stack>
using namespace std;

int n;
int number[10001];//the data set by the user
int a[10001];//memerize the length from the front
int b[10001];//memerize the length from the back

stack<int> s;

//search from front to back
void dfs_front(int i)
{
	//the search rearch to the end
	if (i == n)
	{
		return;
	}

	//update
	if (s.size()>a[i])
		a[i] = s.size();

	//add a[i]
	if (s.empty() || number[i]>s.top())
	{
		s.push(number[i]);
		//update
		if (s.size()>a[i])
			a[i] = s.size();//the LIS before number[i]
		dfs_front(i + 1);//search further
		s.pop();
	}
	dfs_front(i + 1);//search further
}

//search from back to front
void dfs_back(int i)
{
	//the search rearch to the end
	if (i == -1)
	{
		return;
	}

	if (s.size()>b[i])
		b[i] = s.size();
	//add b[i]
	if (s.empty() || number[i]>s.top())
	{
		s.push(number[i]);
		if (s.size()>b[i])
			b[i] = s.size();
		dfs_back(i - 1);//search further
		s.pop();
	}
	dfs_back(i - 1);//search further
}

void handle_result() 
{
	int best = 0;
	int num;
	int sub;
	for (int i = 0; i < n; i++) 
	{
		if (1 == a[i] || 1 == b[i]) //if don't have peak
			continue;
		if (a[i] + b[i] > best)//update the best
		{
			best = a[i] + b[i];
			num = i;
			sub = abs(a[i] - b[i]);
		}
		else if (a[i] + b[i] == best)//update the best for have a better shape
		{
			if ( abs(a[i] - b[i]) < sub) 
			{
				num = i;
				sub = abs(a[i] - b[i]);
			}
		}
	}
	if (0 == best)
	//don't have peak shape
		cout << "No peak shape" << endl;
	else
	//the best has count the peak twice
		cout << best - 1 << " " << num + 1 << " " << number[num] << endl;
}

int main()
{
	cin >> n;
	int i;
	for (i = 0; i < n; i++)
	{
		cin >> number[i];//inilize the number
	}
	dfs_front(0);//search from front
	dfs_back(n - 1);//search from back
	handle_result();//handle the result after the two search

	system("pause");
	return 0;
}


 

方法二 下面两种用动态规划

我们开始考虑这个问题有没有更好的处理方法。我们尝试对这个问题进行分解。

 

对于一个新加入的数m,他能构成的子序列只有两种情况:一种是由只它自己构成也就是长度为1,另一种是它跟在其他子序列的末尾。

那么前一种很容易理解,对于后一种,子序列要满足递增的要求

那么在插入一个新的值的时候,我们需要对已有的子序列进行分析

因为子序列对下一状态的影响只有最后一位,也就是说数m能不能加到这个子序列上取决于最后一位。由此我们可以对以每个数字结尾的子序列长度进行记录

符号标记:A[i]是已有值 D[i]是前k个的LIS长度

 

动态规划DP:

边界 i=n

状态D[k],(k<i)

状态转移方程 D[i+1] = max{ D[j] + 1} ,其中A[j] < A[i+1]

复杂度是O(n^2)


【PAT-T】1017. The Best Peak Shape


/*
this DP1.cpp use DP with O(n^2)
*/
#include<iostream>
using namespace std;

int n;
int number[10001];//the data set by the user
int a[10001];//memerize the length from the front
int b[10001];//memerize the length from the back


//search from front to back
void DP1_front()
{
	for (int i = 0; i < n; i++)
	{
		a[i] = 1;//the min len is 1
		for (int j = 0; j <= i - 1; j++) //search all the num before it
		{
			if (number[j] < number[i] && a[i] < a[j] + 1) //the DP process
			{
				a[i] = a[j] + 1;
			}
		}
	}
	int max=0;
	for (int i = 0; i < n; i++)//set a[i] to be the LIS before a[i]
	{
		if (a[i] > max) max = a[i];
		else  a[i] = max;
	}
}

//search from back to front
void DP1_back() 
{
	for (int i = n - 1; i >= 0; i--)
	{
		b[i] = 1;//the min len is 1
		for (int j = n - 1; j >= i + 1; j--)//search all the num before it
		{
			if (number[j] < number[i] && b[i] < b[j] + 1) //the DP process
			{
				b[i] = b[j] + 1;
			}
		}
	}
	int max=0;
	for (int i = n - 1; i >= 0; i--)//set a[i] to be the LIS before a[i]
	{
		if (b[i] > max) max = b[i];
		else  b[i] = max;
	}
}


void handle_result()
{
	int best = 0;
	int num;
	int sub;
	for (int i = 0; i < n; i++)
	{
		if (1 == a[i] || 1 == b[i]) //if don't have peak
			continue;
		if (a[i] + b[i] > best)//update the best
		{
			best = a[i] + b[i];
			num = i;
			sub = abs(a[i] - b[i]);
		}
		else if (a[i] + b[i] == best)//update the best for have a better shape
		{
			if (abs(a[i] - b[i]) < sub)
			{
				num = i;
				sub = abs(a[i] - b[i]);
			}
		}
	}
	if (0 == best)
		cout << "No peak shape" << endl;
	else
		//the best has count the peak twice
		cout << best - 1 << " " << num + 1 << " " << number[num] << endl;
}

int main()
{
	cin >> n;
	int i;
	for (i = 0; i < n; i++)
	{
		cin >> number[i];//inilize the number
	}
	DP1_front();//search from front
	DP1_back();//search from back
	handle_result();

	system("pause");
	return 0;
}


方法三

对于上面那个例子,当插入进行到2的时候,可以看到3对应的2并没有价值,也就是说,在2[2]之后,以6结尾的这个子序列完全能被以2结尾的子序列取代。

所以显然这个算法存在很多冗余的运算。那么考虑能不能换一种记录,改变传递的状态来优化这种算法,也就是说,在之前的计算中,我们通过记录3和2对应的2,那么要是是我们记录长度2对应的子序列最后一位的最小值,也就是说,在插入2前为3,插入2 后为2。

 

动态规划DP:

符号标记:A[i]是已有值D[i]是前i个的LIS长度M[n]是长度为n的子序列最后一位的最小值

这里的更新方法是通过二分找到大于新插入数的最小值。

 

在这里得到的队列不是最优解,我们可以考虑再开一个堆栈进行记录,当最后一位被修改或者插入的时候修改

【PAT-T】1017. The Best Peak Shape


/*
this DP2.cpp use DP with O(nlogn)
*/
#include<iostream>
using namespace std;

int n;
int number[10001];//the data set by the user
int a[10001];//memerize the length from the front
int b[10001];//memerize the length from the back


//search from front to back
void DP2_front()
{
	int stack[10001];
	int top = 0;
	stack[0] = -10001;
	for (int i = 0; i < n; i++)
	{
		if (number[i] > stack[top])
		{
			stack[++top] = number[i];//push in the stack
		}
		else
		{
			int low = 1, high = top;
			int mid;
			/* search for the first number in stack greater than number[i] */
			while (low <= high)
			{
				mid = (low + high) / 2;
				if (number[i]  > stack[mid])
				{
					low = mid  + 1;
				}
				else
				{
					high = mid - 1;
				}
			}
			/* replace it with number[i] */
			stack[low] = number[i];
		}
		a[i] = top;
	}
}

//search from back to front
void DP2_back()
{
	int stack[10001];
	int top = 0, max = 0;
	stack[0] = -10001;
	for (int i = n-1; i >= 0; i--)
	{
		if (number[i] > stack[top])
		{
			stack[++top] = number[i];//push in the stack
			max++;
		}
		else
		{
			int low = 1, high = top;
			int mid;
			/* search for the first number in stack greater than number[i] */
			while (low <= high)
			{
				mid = (low + high) / 2;
				if (number[i]  > stack[mid])
				{
					low = mid + 1;
				}
				else
				{
					high = mid - 1;
				}
			}
			/* replace it with number[i] */
			stack[low] = number[i];
		}
		b[i] = max;
	}
}


void handle_result()
{
	int best = 0;
	int num;
	int sub;
	for (int i = 0; i < n; i++)
	{
		if (1 == a[i] || 1 == b[i]) //if don't have peak
			continue;
		if (a[i] + b[i] > best)//update the best
		{
			best = a[i] + b[i];
			num = i;
			sub = abs(a[i] - b[i]);
		}
		else if (a[i] + b[i] == best)//update the best for have a better shape
		{
			if (abs(a[i] - b[i]) < sub)
			{
				num = i;
				sub = abs(a[i] - b[i]);
			}
		}
	}
	if (0 == best)
		cout << "No peak shape" << endl;
	else
		//the best has count the peak twice
		cout << best - 1 << " " << num + 1 << " " << number[num] << endl;
}

int main()
{
	cin >> n;
	int i;
	for (i = 0; i < n; i++)
	{
		cin >> number[i];//inilize the number
	}
	DP2_front();//search from front
	DP2_back();//search from back
	handle_result();

	system("pause");
	return 0;
}


 

 

 ##接下来的部分和解题无关

关于如何求那个最优的峰值,峰值意味着状态的改变,相当于是一个截断点。

在这题的截断点可以在最后通过对于所有点进行遍历确定,但在有些情况下这种条件不一定能满足,比如,如果题目的要求里需要有两个峰值。

所以我们思考,在这个过程中能不能先对截断点进行判断。

那么这道题目,一个峰值相当于,两条子序列。假设有k个截断点。原题相当于有1个截断点。接下来的分析我们对于LIS采用nlogn进行分析。

方法一

其中k个点作为截断点的所有情况进行遍历。则复杂度为nk * nlogn

 

方法二

利用LCS

求LIS相当于比如1 3 6 2 4 5 和 123456 的 LCS

这里介绍一下LCS这一个算法

LCS是最长公共子序列

逐行的更新 LCS的算法复杂度为n*m

两个值相等的时候 比较上方和左侧 相等的时候 左上

其实求LIS的过程相当于求这个数列 和 有序数列的 最长公共子序列

那么如果需要求峰形,我们需要的是:

1 3 6 2 4 5 12345654321LCS

所以这种方法的算法复杂度是(n*(k*n));

但是这种方法的一个对于这个问题的缺陷是我们暂时没有想到办法增加约束来保证求得的子序列有k个断点。只能保证,这个子序列有不多于k个,而不是确定的k个。所以我们在写这个程序的时候无法给出是否有峰值的判断。

那么我们依然在这里提到这种方法的原因一个是希望能够提供一种思路,还有一点是,我们在制定测试数据的时候,LIS很难找到实际应用的例子,相较之下,LCS的应用会更加广泛,比如论文查重、填空题评分。所以在这里我们作为一个探究。


方法三

之前的用LCS的算法依然复杂度较高,除此之外空间复杂度也比较高,所以我们进一步考虑有没有什么方法能够降低这个复杂度。

这里为了方便理解我们用两条上升序列来替代上升下降序列

我们考虑通过对于每一个点,它是第几条子序列作为一个独立的维度。

以此,我们能将算法复杂度 k*nlogn

动态规划DP:

初始条件D[0][0] =0 D[1][1] =NULL

边界D[1][n]

状态D[a][b] , ( a < I || a == I , b < j )

状态转移方程   

如果新的数据可以直接加到stack2的末尾

那么从状态一转换而来的大小是从c1+1,状态二是c2+1判断c1和c2的大小

对于图中4的插入

 

如果只能实现更新 ……判断c1+1和c2的大小

对于图中5的插入

好处 复杂度低 可以对读入数据进行实时的处理

坏处 求出的子序列是第一个子序列最长第二个子序列最短,不能求出最好的峰值

 

 【PAT-T】1017. The Best Peak Shape

 

然后最后三种方法没有进行过大数据测试(- -)代码只能提供思路 第二种没有去重



/*
this DP_2_1.cpp search all the possible peak  with O(n^2*logn)
*/
#include<iostream>
using namespace std;

int n;
int number[10001];//the data set by the user
int stack_t[10001];


//the up period
int DP2_up(int m)
{
	int top = 0;
	stack_t[0] = -10001;
	int max = 0;
	for (int i = 0; i <= m; i++)
	{
		if (number[i] > stack_t[top])
		{
			stack_t[++top] = number[i];//push in the stack
		}
		else
		{
			int low = 1, high = top;
			int mid;
			/* search for the first number in stack_t greater than number[i] */
			while (low <= high)
			{
				mid = (low + high) / 2;
				if (number[i]  > stack_t[mid])
				{
					low = mid  + 1;
				}
				else
				{
					high = mid - 1;
				}
			}
			/* replace it with number[i] */
			stack_t[low] = number[i];
		}

	}
	return top;
}

//the down period
int DP2_down(int m)
{
	int top = 0;
	stack_t[0] = 10001;
	int max = 0;
	for (int i = m ; i < n; i++)
	{
		if (number[i] < stack_t[top])
		{
			stack_t[++top] = number[i];//push in the stack_t
		}
		else
		{
			int low = 1, high = top;
			int mid;
			/* search for the first number in stack_t greater than number[i] */
			while (low <= high)
			{
				mid = (low + high) / 2;
				if (number[i]  < stack_t[mid])
				{
					low = mid + 1;
				}
				else
				{
					high = mid - 1;
				}
			}
			/* replace it with number[i] */
			stack_t[low] = number[i];
		}
	}
	return top;
}


void handle_result()
{
	int best = 0;
	int num;
	int sub;
	for (int i = 0; i < n; i++)//if the peak is I
	{
		int up = DP2_up(i), down = DP2_down(i);
		if (1 >= up || 1 >= down) //if don't have peak
			continue;
		if (up + down > best)//update the best
		{
			best = up + down;
			num = i;//update the num
			sub = abs(up - down);//memerize the sub
		}
		else if (up + down== best)//update the best for have a better shape
		{
			if (abs(up - down) < sub)
			{
				num = i;//update the num
				sub = abs(up - down);//memerize the sub
			}
		}
	}
	if (0 == best)
		cout << "No peak shape" << endl;
	else
		cout << best - 1 << " " << num + 1 << " " << number[num] << endl;
}

int main()
{
	cin >> n;
	int i;
	for (i = 0; i < n; i++)
	{
		cin >> number[i];//inilize the number
	}
	handle_result();

	system("pause");
	return 0;
}



/*
this DP_2_2.cpp search possible peak  with O(n^2*logn)
*/

#include<iostream>
#include <algorithm> sort
using namespace std;

int n;
int number[21];//the data set by the user
int a[41];//memerize the length from the front
int c[21][41];

void LCSLength()
{
	//seach the LCS length
	int i, j;
	for (i = 0; i <= n; i++)	//init
		c[i][0] = 0;
	for (j = 1; j <= 2 * n - 1; j++)	//init
		c[0][j] = 0;
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= 2 * n - 1; j++)
		{
			if (number[i - 1] == a[j - 1])			//if eaquals,add 1
			{
				c[i][j] = c[i - 1][j - 1] + 1;
			}
			else if (c[i - 1][j] >= c[i][j - 1])	//if not eaquals,follow the larger in the two
			{
				c[i][j] = c[i - 1][j];
			}
			else
			{
				c[i][j] = c[i][j - 1];
			}
		}
	}
}

void init() 
{
	//perpare for the array a in increasing order
	for (int i = 0; i < n; i++) 
	{
		a[i] = number[i];	
	}
	sort(a, a + n);
	//there comes a decreasing order follow the increasing order
	for (int i = 1; i < n; i++)
	{
		a[n - 1 + i] = a[n - 1 - i];
	}
}


int main()
{
	cin >> n;
	int i;
	for (i = 0; i < n; i++)
	{
		cin >> number[i];//inilize the number
	}
	init();				//build the array in increasing than decreasing order
	LCSLength();		//carry out the proccess of finding the LCS
	cout << c[n][2 * n - 1];

	system("pause");
	return 0;
}



 

/*
this DP_2_3.cpp search possible peak  with O(n^2)
*/
#include<iostream>
using namespace std;

int n;
//int number[10001];//the data set by the user
int stack_1[10001] = { 0 };	//stack for the increasing period
int top1 = 0;
int stack_2[10001] = { 0 };	//stack for the decreasing  period
int top2 = 0;
int all2 = 0;
//update the first period
void LIS_1(int temp)
{
	if (temp > stack_1[top1-1]) //in this case add to the end
	{
		stack_1[top1++] = temp;				//push in the stack
	}
	else //renew the stack
	{
		int low = 0, high = top1 - 1;
		int mid;
		/* search for the first number in stack greater than temp */
		while (low <= high)
		{
			mid = (low + high) / 2;
			if (temp  > stack_1[mid])
			{
				low = mid + 1;
			}
			else
			{
				high = mid - 1;
			}
		}
		/* replace it with temp */
		stack_1[low] = temp;
	}

}

//update the second period
void LIS_2(int temp)
{
	if (temp < stack_2[top2-1]) //in this case add to the end
	{
		stack_2[top2++] = temp;				//push in the stack
	}
	else
	{
		int low = 0, high = top2 - 1;
		int mid;
		/* search for the first number in stack greater than temp */
		while (low <= high)
		{
			mid = (low + high) / 2;
			if (temp  < stack_2[mid])
			{
				low = mid + 1;
			}
			else
			{
				high = mid - 1;
			}
		}
		/* replace it with temp */
		stack_2[low] = temp;
	}

}



int main()
{
	cin >> n;
	int i;
	stack_1[0] = -10001;
	stack_2[0] = -10001;
	int temp;
	for (i = 0; i < n; i++)
	{
 		cin >> temp;

		//prehandle the stack_2 in start
		if (0 == i)
		{
			stack_1[0] = temp;
			top1 = 1;
			continue;
		}
		if (1 == i)
		{
			LIS_1(temp);
			stack_2[0] = temp;
			all2 = 2;
			top2 = 1;
			continue;
		}

		if (stack_2[top2 - 1] > temp)//if the temp can add directly to the end
		{		
			if (top1 < all2)
			{
				LIS_2(temp); 
				all2++;
			}
			else 
			{
				top2 = 1;
				stack_2[0] = temp;
				all2 = top1 + 1;
			}
		}
		else //the temp can only update the stack2 or open a new stack2 
		{
			if (top1 + 1 < all2 )
			{
				LIS_2(temp);
			}
			else
			{
				top2 = 1;
				stack_2[0] = temp;
				all2 = top1 + 1;
			}		
		}
		LIS_1(temp);	//update the stack1
	}
	cout << all2 << endl;
	system("pause");
	return 0;
}