单调队列 POJ2823

七月 8th, 2011 No Comments »

看这个问题:An array of size n ≤ 106 is given to you. There is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves rightwards by one position.Your task is to determine the maximum and minimum values in the sliding window at each position.

也就是有一个数列a,要求你求数列b和c,b[i]是a[i]…a[i+w-1]中的最小值,c[i]是最大值。如果a是1,3,-1,-3,5,3,6,7,则b为-1,-3,-3,-3,3,3,c为3,3,5,5,6,7。

这个问题相当于一个数据流(数列a)在不断地到来,而数据是不断过期的,相当于我们只能保存有限的数据(sliding window中的数据,此题中就是窗口的宽度w),对于到来的查询(此题中查询是每时刻都有的),我们要返回当前滑动窗口中的最大值\最小值。注意,元素是不断过期的。

解决这个问题可以使用一种叫做单调队列的数据结构,它维护这样一种队列:

a)从队头到队尾,元素在我们所关注的指标下是递减的(严格递减,而不是非递增),比如查询如果每次问的是窗口内的最小值,那么队列中元素从左至右就应该递增,如果每次问的是窗口内的最大值,则应该递减,依此类推。这是为了保证每次查询只需要取队头元素。

b)从队头到队尾,元素对应的时刻(此题中是该元素在数列a中的下标)是递增的,但不要求连续,这是为了保证最左面的元素总是最先过期,且每当有新元素来临的时候一定是插入队尾。

满足以上两点的队列就是单调队列,首先,只有第一个元素的序列一定是单调队列。

那么怎么维护这个单调队列呢?无非是处理插入和查询两个操作。

对于插入,由于性质b,因此来的新元素插入到队列的最后就能维持b)继续成立。但是为了维护a)的成立,即元素在我们关注的指标下递减,从队尾插入新元素的时候可能要删除队尾的一些元素,具体说来就是,找到第一个大于(在所关注指标下)新元素的元素,删除其后所有元素,并将新元素插于其后。因为所有被删除的元素都比新元素要小,而且比新元素要旧,因此在以后的任何查询中都不可能成为答案,所以可以放心删除。

对于查询,由于性质b,因此所有该时刻过期的元素一定都集中在队头,因此利用查询的时机删除队头所有过期的元素,在不含过期元素后,队头得元素就是查询的答案(性质a),将其返回即可。

由于每个元素都进队出队一次,因此摊销复杂度为O(n)。

POJ2823就是上面描述的那道题。

下面是代码,很无聊的是这道题竟然卡scanf和printf,如果用cin,cout就会超时,觉得有点无聊。

   1:  #include <iostream>
   2:  #include <vector>
   3:   
   4:  using namespace std;
   5:   
   6:  struct elem 
   7:  {
   8:      int v;
   9:      int p;
  10:  };
  11:   
  12:  int main()
  13:  {
  14:      int l,w;
  15:      cin>>l>>w;
  16:   
  17:      elem* q1 = new elem[l+w];
  18:      elem* q2 = new elem[l+w];
  19:   
  20:      int num,max;
  21:   
  22:      int head1 = 0,tail1 = 0;
  23:      int head2 = 0,tail2 = 0;
  24:      cin>>num;
  25:      q1[tail1].v = num;
  26:      q1[tail1].p = 0;
  27:   
  28:      q2[tail2].v = num;
  29:      q2[tail2].p = 0;
  30:   
  31:      vector<int> ans1,ans2;
  32:   
  33:      for (int t = 1;t < w;t++)
  34:      {
  35:          scanf("%d",&num);
  36:   
  37:          while (head1 <= tail1 && q1[tail1].v >= num)
  38:              --tail1;
  39:          q1[++tail1].v = num;
  40:          q1[tail1].p = t;
  41:   
  42:          while (head2 <= tail2 && q2[tail2].v <= num)
  43:              --tail2;
  44:          q2[++tail2].v = num;
  45:          q2[tail2].p = t;
  46:      }
  47:   
  48:      ans1.push_back(q1[head1].v);
  49:      ans2.push_back(q2[head2].v);
  50:   
  51:      for (int t = w;t < l;t++)
  52:      {
  53:          scanf("%d",&num);
  54:   
  55:          {
  56:   
  57:              while (head1 <= tail1 && q1[tail1].v >= num)
  58:                  --tail1;
  59:              q1[++tail1].v = num;
  60:              q1[tail1].p = t;
  61:   
  62:              while (t - q1[head1].p >= w)
  63:                  ++head1;
  64:   
  65:              ans1.push_back(q1[head1].v);
  66:          }
  67:   
  68:          {
  69:              while (head2 <= tail2 && q2[tail2].v <= num)
  70:                  --tail2;
  71:              q2[++tail2].v = num;
  72:              q2[tail2].p = t;
  73:   
  74:              while (t - q2[head2].p >= w)
  75:                  ++head2;
  76:   
  77:              ans2.push_back(q2[head2].v);
  78:          }    
  79:      }
  80:   
  81:      delete []q1;
  82:      delete []q2;
  83:   
  84:      for (vector<int>::iterator it = ans1.begin();it != ans1.end();it++)
  85:      {
  86:          printf("%d",*it);
  87:          if (it != ans1.end()-1)
  88:              printf(" ");
  89:          else
  90:              printf("\n");
  91:      }
  92:   
  93:      for (vector<int>::iterator it = ans2.begin();it != ans2.end();it++)
  94:      {
  95:          printf("%d",*it);
  96:          if (it != ans2.end()-1)
  97:              printf(" ");
  98:          else
  99:              printf("\n");
 100:      }
 101:  }

网络最大流、二分图最大匹配、POJ2536

十二月 14th, 2010 No Comments »

通常我们把一个有向无环图边上的权值看做两个节点之间的距离,在这个模型下的问题有各种最短路问题。如果将边上的权值不看做距离而看做两点间的容量(比如两个城市之间一天的运输能力),这样的有向无环图就叫做流网络,对应的问题就是求最大流——单位时间内通过网络的最大容量(比如工厂所在城市一天最多可以生产多少才能全部经过一个流网络运到仓库所在城市而不造成中途的淤积)。

流网络只有一个源点和一个汇点。关于定义就不说了,算法导论上很清楚。有一点要说的是,如果一个流网络有多个源点和多个汇点,可以通过增加一个到所有现有源点的容量都无穷的超级源点和一个所有汇点到其容量都无穷的超级汇点转化成只有一个源点和一个汇点的流网络。

求流网络的最大流算法是Ford-Fulkerson算法。首先定义一条边的残留容量是容量减去现有流,由残留容量标示的网络称为残留网络。再定义增广路径是残留网络中从源点s到汇点t的一条路径。Ford-Fulkerson算法是一个迭代过程。每次在残留网络中查找一条增广路径,然后把这条增广路径中最小的一段残留容量加到增广路径各条边的流上,更新残留网络继续迭代,直到找不到增广路径为止。此时退出迭代,现有流就是最大流。

二分图有两部分节点L和R,各部分内部节点之间没有边,即每条边的两个节点都一定分属这两部分,二分图的一个匹配是找到这样一组边,使得每个节点都只有至多一条边与其相连。

二分图的最大匹配问题可以转化为网络最大流问题。增加一个到所有L中顶点容量均为1的源点s和一个所有R中顶点到其容量均为1的汇点t,所有L到R中的边容量也设置为1,现在查找此网络流的最大流就等同于求此二分图的最大匹配。证明过程见算法导论。

POJ2536就是一道二分图最大匹配的应用。题目给出n个老鼠的坐标和m个鼠洞的坐标,如果一只老鼠能在一定时间内到达一个鼠洞,就能躲过老鹰。所以如果某个老鼠与某个鼠洞之间距离小于某值,它俩之间就存在一条边,否则不存在,这是一个明显的二分图。而要求有多少老鼠能躲过老鹰,自然是求此二分图的最大匹配。

代码如下: Continue reading »

POJ1050 To The Max,最大子串扩展,DP

四月 16th, 2010 No Comments »
To the Max
Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 19640   Accepted: 10158

Description

Given a two-dimensional array of positive and negative integers, a sub-rectangle is any contiguous sub-array of size 1*1 or greater located within the whole array. The sum of a rectangle is the sum of all the elements in that rectangle. In this problem the sub-rectangle with the largest sum is referred to as the maximal sub-rectangle.
As an example, the maximal sub-rectangle of the array:

0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
is in the lower left corner:

9 2
-4 1
-1 8
and has a sum of 15.

Input

The input consists of an N * N array of integers. The input begins with a single positive integer N on a line by itself, indicating the size of the square two-dimensional array. This is followed by N^2 integers separated by whitespace (spaces and newlines). These are the N^2 integers of the array, presented in row-major order. That is, all numbers in the first row, left to right, then all numbers in the second row, left to right, etc. N may be as large as 100. The numbers in the array will be in the range [-127,127].

Output

Output the sum of the maximal sub-rectangle.

Sample Input

4
0 -2 -7 0 9 2 -6 2
-4 1 -4  1 -1

8  0 -2

Sample Output

15

Source

题意是给出一个方阵,各元素有正有负,用一个矩形将其中的部分元素括起来,使内部元素之和最大,求此最大和。
如果不是方阵而是一维数组,显然是最大子串问题。
插一句,最大子串问题的DP方程为dp[i] = dp[i-1]+numbers[i]>0?dp[i-1]+numbers[i]:0
其中,dp[i]表示以元素i结尾的最大子串大小,求解整个dp数组后遍历便可得到最大子串大小。
回到这个问题。
如果已经确定这个矩形应该包含哪些行,而还不知道该包含哪些列,那么可以将每一列的各行元素相加,从而将矩阵转换为一维数组的最大子串问题。因此,只要我们枚举矩阵应该从哪一行到哪一行,就可以将问题用最长子串策略来求解了。
代码如下:
#include <iostream>
 
using namespace std;
 
int numbers[105][105],dp[105],n,temp[105];
 
int main()
{
	cin>>n;
	for (int i = 1;i <= n;i++)
		for (int j = 1;j <= n;j++)
			cin>>numbers[i][j];
	int Max = 0;
	for (int i = 1;i <= n;i++)	//start from line i
	{
		memset(temp,0,sizeof(temp));
		for (int j = i;j <= n;j++)	//end at line j
		{
			for (int k = 1;k <= n;k++)
			{
				temp[k] += numbers[j][k];
 
				if (temp[k] + dp[k-1] > 0)
					dp[k] = temp[k] + dp[k-1];
				else
					dp[k] = 0;
 
				if (Max < dp[k])
					Max = dp[k];
			}
		}
	}
	cout<<Max<<endl;
}

POJ 1631 DP+最长上升子序列+二分查找

四月 16th, 2010 No Comments »
Bridging signals
Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 5191   Accepted: 2867

Description

‘Oh no, they’ve done it again’, cries the chief designer at the Waferland chip factory. Once more the routing designers have screwed up completely, making the signals on the chip connecting the ports of two functional blocks cross each other all over the place. At this late stage of the process, it is too expensive to redo the routing. Instead, the engineers have to bridge the signals, using the third dimension, so that no two signals cross. However, bridging is a complicated operation, and thus it is desirable to bridge as few signals as possible. The call for a computer program that finds the maximum number of signals which may be connected on the silicon surface without crossing each other, is imminent. Bearing in mind that there may be thousands of signal ports at the boundary of a functional block, the problem asks quite a lot of the programmer. Are you up to the task?

A typical situation is schematically depicted in figure 1. The ports of the two functional blocks are numbered from 1 to p, from top to bottom. The signal mapping is described by a permutation of the numbers 1 to p in the form of a list of p unique numbers in the range 1 to p, in which the i:th number specifies which port on the right side should be connected to the i:th port on the left side.Two signals cross if and only if the straight lines connecting the two ports of each pair do.

Input

On the first line of the input, there is a single positive integer n, telling the number of test scenarios to follow. Each test scenario begins with a line containing a single positive integer p < 40000, the number of ports on the two functional blocks. Then follow p lines, describing the signal mapping:On the i:th line is the port number of the block on the right side which should be connected to the i:th port of the block on the left side.

Output

For each test scenario, output one line containing the maximum number of signals which may be routed on the silicon surface without crossing each other.

Continue reading »

威佐夫博弈、黄金分割、POJ 1067

四月 3rd, 2010 2 Comments »
取石子游戏
Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 17630 Accepted: 5338

Description

有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。现在给出初始的两堆石子的数目,如果轮到你先取,假设双方都采取最好的策略,问最后你是胜者还是败者。

Input

输入包含若干行,表示若干种石子的初始情况,其中每一行包含两个非负整数a和b,表示两堆石子的数目,a和b都不大于1,000,000,000。

Output

输出对应也有若干行,每行包含一个数字1或0,如果最后你是胜者,则为1,反之,则为0。

Sample Input

2 1
8 4
4 7

Sample Output

0
1
0

Source

这个游戏就是所谓的威佐夫博弈(Wythoff Game),很有意思。

昨天就在做这道题目,思考成果是:用一个二维数组来表示玩家将面临的局面,即a[i][j]表示两堆石头分别有i个和j个,如果a[i][j]是必败局面的话,那么——这个点的右侧所有点a[i][k](k>j)都是必胜点,因为可以通过拿走第二堆的k-j个石子令对方面临必败局面a[i][j];这个点的下侧所有点a[k][j](k>i)都是必胜点,因为可以通过拿走第二堆的k-i个石子令对方面临必败局面a[i][j];这个点的右下方45度所有点a[i+k][j+k](k>0)都是必胜点,因为可以通过同时拿走两堆的k个石子令对方面临必败局面a[i][j]。这样如果在二维数组a[i][j]处标记”X”表示必败的话,在上面提到的三类点的位置都可以标记”O”表示必胜了,做完这项工作后,再挑选如今距离原点最近的未被标记的点,它一定是下一个必败点——因为它无法通过游戏规则移动到一个必败点,并且规则规定的动作都是朝向原点移动的,而它是距离原点最近的未被标记的点,因此它只能移动到一个必胜点从而让对方获胜,所以该点一定是下一个必败点。有了新必败点后就可以重复上述工作,直到找出问题范围内的所有必败点。

然而这样的模拟工作是不被允许的,因为数据量太大了,因此这道题要求的是给定两个数i,j,有办法立刻判断a[i][j]是不是必败点,在O(1)时间给出解答。

我试图找到必败点的规律,但是没成功,以上就是昨天的思考结果。因为这个问题够简洁,所以我坚信一定有简单规律的,因此在考察了很大数据量仍然没有发现规律后,我无比好奇这道题的解法。今天上网一搜才知道规律果然够简单够经典,竟然是“黄金分割”,以下内容来自对网上内容的总结:

前几个必败点如下:(0,0),(1,2),(3,5),(4,7),(6,10),(8,13)……可以发现,对于第k个必败点(m(k),n(k))来说,m(k)是前面没有出现过的最小自然数,n(k)=m(k)+k。一个必败点有如下性质:

1.所有自然数都会且仅会出现在一个必败点中;

证明:m(k)是前面没有出现过的最小自然数,自然与前k-1个必败点中的数字都不同;m(k)>m(k-1),否则违背m(k-1)的选择原则;n(k)=m(k)+k>m(k-1)+(k-1)=n(k-1)>m(k-1),因此n(k)比以往出现的任何数都大,即也没有出现过。又由于m(k)的选择原则,所有自然数都会出现在某个必败点中。性质1证毕。

2.规则允许的任意操作可将必败点移动到必胜点;

证明:以必败点(m(k),n(k))为例。若只改变两个数中的一个,由于性质1,则得到的点一定是必胜点;若同时增加两个数,由于不能改变两数之差,又有n(k)-m(k)=k,故得到的点也一定是必胜点。性质2证毕。

3.一定存在规则允许的某种操作可将必胜点移动到必败点;

证明:以某个必胜点(i,j)为例。因为所有自然数都会出现在某个必败点中,故要么i等于m(k),要么j等于n(k)。若i=m(k),j>n(k),可从j中取走j-n(k)个石子到达必败点;若i=m(k),j<n(k),可从两堆同时拿走m(k)-m(j-m(k)),从而到达必败点(m(j-m(k)),m(j-m(k))+j-m(k));若i>m(k),j=n(k),可从i中取走i-m(k)个石子到达必败点;若i<m(k),j=n(k),需要再分两种情况,因为i一定也出现在某个必败点中,若i=m(l),则从j中拿走j-n(l),若i=n(l),则从j中拿走j-m(l),从而到达必败点(m(l),n(l))。性质3证毕。

判断一个点是不是必败点的公式与黄金分割有关,为:

m(k) = k * (1 + sqrt(5))/2

n(k) = m(k) + k

至于为什么如此,我就不知道了,也没有查到,很好奇。

POJ1067就是这道题,代码如下:

#include 
#include 
 
using namespace std;
 
int main()
{
	int m,n;
	while(cin>>m>>n)
	{
		if (m > n)
		{
			int temp;
			temp = m;
			m = n;
			n =temp;
		}
		int k = n - m;
		int data = floor(k*(1.0+sqrt(5.0))/2.0);
		if (data == m)
			cout<<0<<endl;
        }
}
需要注意的是,由于数据量过大,float的精度不够,而只能用double,因此代码中的小数没有加“f”。

动态规划 POJ 1661

四月 2nd, 2010 No Comments »
Help Jimmy
Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 5193   Accepted: 1582

Description

“Help Jimmy” 是在下图所示的场景上完成的游戏。

场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。

Jimmy老鼠在时刻0从高于所有平台的某处开始下落,它的下落速度始终为1米/秒。当Jimmy落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是1米/秒。当Jimmy跑到平台的边缘时,开始继续下落。Jimmy每次下落的高度不能超过MAX米,不然就会摔死,游戏也会结束。

设计一个程序,计算Jimmy到底地面时可能的最早时间。

Input

第一行是测试数据的组数t(0 <= t <= 20)。每组测试数据的第一行是四个整数N,X,Y,MAX,用空格分隔。N是平台的数目(不包括地面),X和Y是Jimmy开始下落的位置的横竖坐标,MAX是一次下落的最大高度。接下来的N行每行描述一个平台,包括三个整数,X1[i],X2[i]和H[i]。H[i]表示平台的高度,X1[i]和X2[i]表示平台左右端点的横坐标。1 <= N <= 1000,-20000 <= X, X1[i], X2[i] <= 20000,0 < H[i] < Y <= 20000(i = 1..N)。所有坐标的单位都是米。

Jimmy的大小和平台的厚度均忽略不计。如果Jimmy恰好落在某个平台的边缘,被视为落在平台上。所有的平台均不重叠或相连。测试数据保证问题一定有解。

Output

对输入的每组测试数据,输出一个整数,Jimmy到底地面时可能的最早时间。

Sample Input

1
3 8 17 20
0 10 8
0 10 13
4 14 3

Sample Output

23

Source

今天做了几道DP的题,觉得有点心得,这道题又有些可写之处,就来一篇吧。
此题为典型的动态规划,一开始我思考的复杂了,或者说思考的不够深入导致没有简化彻底,就着手写代码了。
所谓动态规划,就是递归(或者递推)+存储。动态规划能解决的问题具有最有子结构,因此很自然就可以得到递归解,但是由于重复计算的原因,递归的效率很低,如果将计算过的结果存储起来以保证每个子问题只计算一次,就得到了动态规划的基本思想。如果合理安排各个子问题的计算顺序,就能保证计算每一个子问题的时候,求解它所依赖的其它子问题都已经求解完毕。
动态规划的一般步骤是:a.划分子问题;b.找问题状态(能代表子问题的变量,可能是一维,也可能是一维以上);c.找状态转移关系(动态规划的灵魂就是状态转移方程);d.按合理顺序求解各子问题直至原问题得解。
这道题中,如果将所有平板按照高度进行排序(地板为0,最高为n),则可以用板子的编号i作为状态变量,每个状态代表两个子问题——从该板左端点到达地面所需最短时间,和从该板右端点到达地面所需最短时间,如果求出了这两个子问题,那么当Jimmy从上一个板子到达该板某处(!)时,再到达地面的最短时间就很容易求了,或者说Jimmy从上一个板子的左侧或者右侧(!)开始到达地面的最短时间就很容易求了。于是,我们令子问题为f[i].fromLeft和f[i].fromRight,分别代表从i号板左侧和右侧到达地面所需最短时间。
有了子问题划分和状态描述,状态转移方程就很容易看出来了:
if (板子i的左侧下方Max以内的距离有其它板子j)
	f[i].fromLeft=i高度-j高度+max(i的X走到j左侧的时间+f[j].fromLeft,i的X走到j右侧的时间+f[j].fromRight);
else
	f[i].fromLeft=正无穷;

这样本题的思路就很清晰了。
刚做此题的时候,没有思考透彻,以为从一个板子跳时,有很多个选择,而忽略了板子之间的互相遮挡,结果考虑得很复杂,其实从任何一个板子的某个端点起跳,最多只能落到一个板子上,因此,在每一个板子上只有两个选择——向左跳还是向右跳,而不存在向左跳到哪个板和向右跳到哪个板的问题。

代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
 
using namespace std;
 
#define INF 9999999
 
struct _platform 
{
	int x1;
	int x2;
	int height;
};
 
bool UDgreater(_platform p1,_platform p2)
{
	return p1.height < p2.height;
}
 
struct _time
{
	int fromLeft;
	int fromRight;
};
 
int N,X,Y,MAX;
vector<_platform> pvec;
_time fastest[1005];
 
void prepare()
{
	memset(fastest,INF,sizeof(fastest));
	pvec.clear();
	cin>>N>>X>>Y>>MAX;
 
	_platform platform;
	platform.x1 = -INF;
	platform.x2 = INF;
	platform.height = 0;
	pvec.push_back(platform);
 
	for (int i = 1;i <= N;i++)
	{
		cin>>platform.x1>>platform.x2>>platform.height;
		if (platform.height <= Y)
		{
			pvec.push_back(platform);
		}
	}
	platform.x1 = X;
	platform.x2 = X;
	platform.height = Y;
	pvec.push_back(platform);
	sort(pvec.begin(),pvec.end(),UDgreater);
}
 
bool CanLoad(int i,int j,int dir)
{	//whether Jimmy can load on j from i safely;0 for left,1 for right
	if (dir == 0)
	{
		if (pvec[i].height-pvec[j].height <= MAX&&\
			pvec[i].x1>=pvec[j].x1&&pvec[i].x1<=pvec[j].x2)
			return true;
	}
	else
	{
		if (pvec[i].height-pvec[j].height <= MAX&&\
			pvec[i].x2>=pvec[j].x1&&pvec[i].x2<=pvec[j].x2)
			return true;		
	}
	return false;
}
 
int main()
{
	int n;
	cin>>n;
	while (n--)
	{
		prepare();
		fastest[0].fromLeft = 0;
		fastest[0].fromRight = 0;
		int size = pvec.size();
		for (int i = 1;i < size;i++)
		{
			int leftMin = INF,rightMin = INF;
			int j = i-1;
			while(j>=0&&!CanLoad(i,j,0))
				j--;
			if (j > 0)
				fastest[i].fromLeft = min(fastest[j].fromLeft+\
				abs(pvec[i].x1-pvec[j].x1),fastest[j].fromRight+\
				abs(pvec[i].x1-pvec[j].x2))+abs(pvec[i].height-pvec[j].height);
			else if(j == 0)
				fastest[i].fromLeft = abs(pvec[i].height-pvec[j].height);
			j = i-1;
			while (j>=0&&!CanLoad(i,j,1))
				j--;
			if (j > 0)
				fastest[i].fromRight = min(fastest[j].fromLeft+\
				abs(pvec[i].x2-pvec[j].x1),fastest[j].fromRight+\
				abs(pvec[i].x2-pvec[j].x2))+abs(pvec[i].height-pvec[j].height);				
			else if(j == 0)
				fastest[i].fromRight = abs(pvec[i].height-pvec[j].height);
		}
		cout<<min(fastest[size-1].fromLeft,fastest[size-1].fromRight)<<endl;
	}
}