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;
}