第五周 单调栈、单调队列、尺取、差分、画图

5.1单调栈解决矩形面积
输入包含多组数据。每组数据用一个整数n来表示直方图中小矩形的个数,你可以假定1 <= n <= 100000. 然后接下来n个整数h1, …, hn, 满足 0 <= hi <= 1000000000.
这些数字表示直方图中从左到右每个小矩形的高度,每个小矩形的宽度为1。 测试数据以0结尾。

输入样例 :
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
0

输出样例:
8
4000

解题的方法:利用单调栈可在O(n)的时间复杂度中解决这个问题
构建递减栈 (栈顶的元素是最大的)。

解题的思路:对每一个高度所包含的矩形面积进行统计,用maxrear来保存最大面积

实现:本题目中矩形高度不同,但所有的矩形宽度均为1。
对于一个节点而言,有两个性质,高和宽。
我们实现的是对面积进行统计,因而我们应该找左宽与右宽。
立马想到的是我们可以利用单调栈两次,分别统计左宽与右宽。
但在这里的实现中,我们只利用了一次单调栈。
我们对右宽进行统计,左宽放在节点的宽度值中。

入栈:只有大于栈顶元素,才能进行顺利入栈。
如果出现小于栈顶元素的情况,这个时候进行出栈。
出栈的过程中,对矩形的面积进行统计。
该矩形的右宽用sum统计,左宽与自身宽利用该矩形节点的宽度记录。
面积便是(左宽+右宽)*高。
到了可以入栈的时候,入栈元素的左宽为刚出栈的栈顶元素的宽度,即此时sum。
故存储宽度为(sum+1)。

面积:在全部元素的入栈过程中,出栈一次对面积计算一次,用maxrear进行统计。
最后栈中元素全部出栈,和maxrear进行比较、替换、统计。
当栈中元素全部出栈后,输出此时的最大面积。

#include <iostream>
#include<cstdio>
#include<string>
#include<algorithm>
using namespace std;

 
struct node
{
	int h;
	int w;
};

node stack[100005];

int main()
{ 
  
  int n,top; 
  long long maxrear;
  stack[0].h=-1;
  stack[0].w=0;
  
   
   
  while(scanf("%d",&n),n)
 {
 	  maxrear=0,top=0;
	  for(int i=0;i<n;i++)
	  {
	    node s;
		s.w=1;
		scanf("%d",&s.h);
		
		if(s.h>stack[top].h)
		{ 
			stack[++top].h=s.h;
			stack[top].w=s.w; 
		}  	
		
		else
		{
		  long long sum=0;
	      while(s.h<=stack[top].h)
		  {
		  	sum+=stack[top].w;     //保留当前栈顶的宽度 
		  	if(maxrear<sum*stack[top].h) maxrear=sum*stack[top].h;
		  	top--;
		  }		
		  s.w=sum+1;
		  stack[++top].h=s.h;
		  stack[top].w=s.w;  
		}
	  }  
	  
	  long long sum=0;
	  while(top>0)
	  {
	  	sum+=stack[top].w ;
	  	if(maxrear<sum*stack[top].h) maxrear=sum*stack[top].h;
	  	top--;
	  }  	
	  printf("%lld\n",maxrear);
 }
 return 0;
} 


5.2.差分法解决魔术猫
差分数组解决问题
利用差分数组可解决的问题为将区间修改变为单点修改。
如果B[]数组是A[]数组的差分数组,则A的区间修改转化为B的单点修改如下。
A[L, R] += c B[L] += c, B[R+1] -= c
关系有:A[i]=B[i]+A[i-1]=B[i]+B[i-1]+A[i-2]
依次推导可知:B数组前缀和即为 A数组最终数值

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

const int maxn=200000+10;
int n,q,a[maxn],b[maxn];
//a是原始数据,b是差分数组
 
int main()
{
    cin>>n;
	cin>>q; 
    for (int i=1; i<=n; i++)
        scanf("%d",&a[i]);
    for (int i=1; i<=n; i++)//求差分
        b[i]=a[i]-a[i-1];
    for (int i=1; i<=q; i++)
    {
        int l,r,c;
        cin>>l>>r>>c;
        b[l]+=c; b[r+1]-=c;
    }
    long long ans=0;
    for (int i=1; i<=n; i++)//前缀和将差分转换回去
    {
        ans+=b[i];
        printf("%lld ",ans);
    }
    cout<<endl; 

    return 0;
}
 

5.3.尺取法解决字符替换
尺取法:上课讲的时候,助教将其成为毛毛虫法,类似于虫子爬行,两端都动

课上的方法为:拿出区间,统计区间四个字符中外的最大出现次数,
计算其他三个与最大次数的差距,计算差距和。
用区间内的字符将差距补齐。
若区间内剩余字符的数目为4的倍数,则可完成替换,否则不可完成。

这里的实现:是通过四字符的总数目的均值差(只统计正)与区间内的四字符数目进行比较。
假如区间内的该字符数目大于均值差,说明可在区间内调整使该字符等于均值。
如果四个字符均可调整,那说明整体字符替换成立。
这个时候,输入字符其实应该是4的倍数。

区间如何移动呢?区间每次向左或右移动一个单位。
四字符的总数目的均值差不变,但区间内的字符数目发生变化,进行重新统计。
直到右边界溢出时,统计进行终止。
答案的得出:每次统计的过程,区间符合且小于当前答案,进行替换。最终得出正确结果。

#include <iostream>
using namespace std;
 
int balance(string s)
{
    int record[4] = {0,0,0,0};
    int len = s.length();
    int ans = INT_MAX;
    for(int i = 0; i < len; i++)
    {
        if(s[i] == 'Q')
            record[0]++;
        if(s[i] == 'W')
            record[1]++;
        if(s[i] == 'E')
            record[2]++;
        if(s[i] == 'R')
            record[3]++;
    }
  
    int average = s.length()/4;
    int d = 0;
    for(int i = 0; i < 4; i++)
    {
        if(record[i] > average)
        {
            record[i]=record[i]-average;
            d += record[i];
        }
        else
        {
            record[i]=0;
        }
    }

    if(d == 0)
        return 0;

    int left = 0;
    int right = 1;
    int rec[4] = {0,0,0,0};
    for(int i = left; i <= right; i++)
    {
        if(s[i] == 'Q')
            rec[0]++;
        if(s[i] == 'W')
            rec[1]++;
        if(s[i] == 'E')
            rec[2]++;
        if(s[i] == 'R')
            rec[3]++;
    }
    while(right < len)
    {
        if(ans == d)
            break;

        if(rec[0] >= record[0] && rec[1]>=record[1] && rec[2]>=record[2] && rec[3]>= record[3])
        {
            if((right-left)<ans)
                ans=right-left+1;

            if(s[left] == 'Q')
                rec[0]--;
            if(s[left] == 'W')
                rec[1]--;
            if(s[left] == 'E')
                rec[2]--;
            if(s[left] == 'R')
                rec[3]--;
            left++;
        }
        else
        {
            right++;
            if(s[right] == 'Q' && right != len)
                rec[0]++;
            if(s[right] == 'W' && right != len)
                rec[1]++;
            if(s[right] == 'E' && right != len)
                rec[2]++;
            if(s[right] == 'R' && right != len)
                rec[3]++;
        }
    }

    return ans;
}
 
int main()
{
	int number;
	string a;
	cin>>a;
	number=balance(a);
	cout<<number<<endl; 
} 

5.4.单调队列解决滑动窗口
ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动.
现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如:
数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
Window position Minimum value Maximum value
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7

注意:10^6,cin与scanf的时间相差2秒,因而大量数据的读入通常用scanf

单调队列:头、尾都可以移动,单调的说法在于队头的元素是最大还是最小值。
以单增队列(队头最小为例说明)。
当队列为空的时候(看head与tail的值比较)
将队尾后移,这里通过数组下标实现后移。
当队不为空,且队中的数目大于窗口时,对头前移。
当队不为空,入队元素小于等于队尾时,队尾前移、弹出不符合条件元素。
用Min数组统计此时队尾对应的数组下标。
当处理数目大于等于窗口范围,小于等于数组总数时,输出此时队首。

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
 

int n,k,a[1000010],Max[1000010],Min[1000010];
int head=1,tail=0;
 
int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
    
	for(int i=1;i<=n;i++)
	{
		while(head<=tail && k<i-Min[head]+1)
		{
		 head++;
	    }
		while(head<=tail&&a[i]<=a[Min[tail]])
		{
		 tail--;
	    }
		Min[++tail]=i;
		if(i>=k && i<=n)
		{
		 printf("%d ",a[Min[head]]);
	    }
	}
	
	printf("\n");
	head=1,tail=0;
	for(int i=1;i<=n;i++)
	{
		while(head<=tail&&Max[head]+k<=i)
		{
		 head++;
	    }
		while(head<=tail&&a[i]>=a[Max[tail]])
		{
		 tail--;
	    }
		Max[++tail]=i;
		if(i>=k && i<=n)
		{
		 printf("%d ",a[Max[head]]);
	    }
	}	
	return 0;
}


5.5.画图
问题描述
用 ASCII 字符来画图是一件有趣的事情,并形成了一门被称为 ASCII Art 的艺术。
本题要求编程实现一个用 ASCII 字符来画图的程序,支持以下两种操作:
画线:给出两个端点的坐标,画一条连接这两个端点的线段。
简便起见题目保证要画的每条线段都是水平或者竖直的。
水平线段用字符 - 来画,竖直线段用字符 | 来画。
如果一条水平线段和一条竖直线段在某个位置相交,则相交位置用字符 + 代替。
填充:给出填充的起始位置坐标和需要填充的字符,
从起始位置开始,用该字符填充相邻位置,直到遇到画布边缘或已经画好的线段。
注意这里的相邻位置只需要考虑上下左右 4 个方向。

输出格式
输出有n行,每行m个字符,表示依次执行这q个操作后得到的画图结果。
样例输入
4 2 3
1 0 0 B
0 1 0 2 0
1 0 0 A

样例输出
AAAA
A–A

问题分析:该题是对字符的存储与操作。

1.对于该问题,我们如何进行存储?
由题目中的输出样例,我们可以知道采用二维数组对图型进行存储。但存在一个问题,
在这个图中,左下角的坐标为(0,0),但在二维数组中(0,0)位置我们将其视为左上角。
这个时候我们应将y(纵坐标)进行转化,则对于纵坐标为y的点,我们在数组中存放的位置为(n-1-y),这个时候我们实现了对数据的存储转化。

2.对于该问题中的画线操作说明。
在图形中画线存在两种情况,x相同画一条横线(-),y相同画一条(|)线。/以画一条横线为例:我们应先确定画线的区间,通过x1与x2分别区间的最大与最小值。
确定了要画线的点后,我们对点进行分析,以确定如何画线。对于画横线而言,该点是
竖线或加号时,一律统一该点为(+)。将区间内的所有点进行处理。画竖线类似。

3.对于该问题的填充操作说明。
我们用字符进行填充时,我们对这个点的判断为:
1.该点是否溢出边界。
2.该点是否已经被线段填充。
3.该点是否被此次相同字符填充。
判断完后对该点进行是否填充的判断。如果填充,向该点的上、下、左、右四个方向拓展。向四个方向拓展的实现的方法是通过宽度优先搜索的递归方式。

#include<bits/stdc++.h>
using namespace std;

char mp[105][105];//整个图
int m,n,q,num;     //m列,n行,q个操作,num判断是画线还是填充 
int dir[4][2]={{-1,0},{0,1},{1,0},{0,-1}};//4个方向

void DFS(int i,int j,char c)
{//填充字符
    if(i<0||i>=n||j<0||j>=m//超出画布
            ||mp[i][j]=='-'||mp[i][j]=='|'||mp[i][j]=='+'//是线段的一部分
            ||mp[i][j]==c)//已经填充过
        return;//直接返回
    mp[i][j]=c;//进行填充
    for(int k=0;k<4;++k)//递归处理4个周边坐标
        DFS(i+dir[k][0],j+dir[k][1],c);
}
int main()
{
    scanf("%d%d%d",&m,&n,&q);
    for (int i=0;i<=105;i++)
		for (int j=0;j<=105;j++)
		{
			mp[i][j]='.';
		}
    while(q--)
	{
        scanf("%d",&num);
        if(num==0)
		{//画线
            int x1,x2,y1,y2;
            scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
            if(x1>x2)//x1存储x1和x2之间的最小值,x2存储x1和x2之间的最大值
                swap(x1,x2);
            if(y1>y2)//y1存储y1和y2之间的最小值,y2存储y1和y2之间的最大值
                swap(y1,y2);
            if(x1!=x2)
                for(int i=x1;i<=x2;++i)
                    if(mp[n-1-y1][i]=='|'||mp[n-1-y1][i]=='+')
                        mp[n-1-y1][i]='+';
                    else
                        mp[n-1-y1][i]='-';
            else
                for(int i=y1;i<=y2;++i)
                    if(mp[n-1-i][x1]=='-'||mp[n-1-i][x1]=='+')
                        mp[n-1-i][x1]='+';
                    else
                        mp[n-1-i][x1]='|';
        }
		
		else
		{//填充字符
            int x,y;
            char c;
            scanf("%d%d %c",&x,&y,&c);
            DFS(n-1-y,x,c);
        }
    }
    
    for(int i=0;i<n;++i)
	{
        for(int j=0;j<m;++j)
            printf("%c ",mp[i][j]);
        
    }
    return 0;
}
  
在Python中,单调栈单调队列是两种不同的数据结构。单调栈是一个栈,它的特点是栈内的元素是单调的,可以是递增或递减的。在构建单调栈时,元素的插入和弹出都是在栈的一端进行的。与此类似,单调队列也是一个队列,它的特点是队列内的元素是单调的,可以是递增或递减的。在构建单调队列时,元素的插入是在队列的一端进行的,而弹出则是选择队列头进行的。 单调队列在解决某些问题时,能够提升效率。例如,滑动窗口最大值问题可以通过使用单调队列来解决。单调队列的结构可以通过以下代码来实现: ```python class MQueue: def __init__(self): self.queue = [] def push(self, value): while self.queue and self.queue[-1 < value: self.queue.pop(-1) self.queue.append(value) def pop(self): if self.queue: return self.queue.pop(0) ``` 上述代码定义了一个名为MQueue的类,它包含一个列表作为队列的存储结构。该类有两个方法,push和pop。push方法用于向队列中插入元素,它会删除队列尾部小于插入元素的所有元素,并将插入元素添加到队列尾部。pop方法用于弹出队列的头部元素。 总结来说,单调栈单调队列都是为了解决特定问题而设计的数据结构。单调栈在构建时元素的插入和弹出都是在栈的一端进行的,而单调队列则是在队列的一端进行的。在Python中,可以通过自定义类来实现单调队列的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值