程序设计思维与实践 Week5 作业

A 最大矩形

1. 题目大意

给一个直方图,求直方图中的最大矩形的面积。例如,下面这个图片中直方图的高度从左到右分别是2, 1, 4, 5, 1, 3, 3, 他们的宽都是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

2. 思路历程

  • 要求出直方图中矩形的最大面积,应对每一个位置的宽和高进行考虑,防止遗漏。
  • 确定了长方形的位置即确定了矩形的高,因此矩形的宽度越大面积就越大。
  1. 需要最大的宽度,考虑左端点向左延伸和右端点向右延伸的最大值
  2. 如果相邻的长方形的高度小于此高度,则可以把矩形的宽向该处延伸,因此
    1)左端点可以确定为往左数第一个小于此高度的点
    2)右端点可以确定为往右数第一个小于此高度的点
  • 此处引入单调栈这一数据结构
  1. 单调栈即维护了单调性的栈,满足栈的性质,栈中元素自栈顶到栈底满足单调性
  2. 在入栈过程中就维护单调性。如递增栈入栈时,若栈为空或者栈顶元素大于入栈元素可入栈,否则需将不满足条件的栈顶元素全部弹出后才可入栈
  3. 单调递增(减)栈可以找到往左/往右第一个比当前元素大(小)的元素(当前的栈底元素即为所求元素)— — 可以求得以当前元素为最值的最大区间
  • 两遍单调栈可以得到每个位置的左右端点。

3. 具体实现

  • 创建数组L和R用于记录每个位置可以延伸的左右端点,i位置的左端点为L[i],右端点为R[i]
  • 函数find_left()和函数find_right()分别用于得到每个位置的左端点和右端点。
  1. find_right()函数中维护了一个递减栈,记录下标而非记录元素是为了方便计算宽度
  2. 从0到n-1位置遍历长方形,按照单调顺序入栈
    1)若栈为空或满足单调性则直接入栈
    2)否则弹出不满足的元素并确定它们的R[i] = i - 1(i首次破坏了单调性,则i-1最后一次满足了单调性)
  3. 最后当栈非空时,处理这些延伸到边界的长方形
  4. find_left()函数则从n-1到0遍历长方形,其他处理完全相同
  • 每个位置的宽即为R[i] - L[i] + 1,记录最大面积即可。

4. 代码

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

int n;
long long hi[100011], L[100011], R[100011];

void find_right()
{
    stack<int> right;
    for (int i = 0; i < n; i++)
    {
        while (!right.empty() && hi[right.top()] > hi[i])
        {
            R[right.top()] = i - 1;
            right.pop();
        }
        right.push(i);
    }
    while (!right.empty())
    {
        R[right.top()] = n - 1;
        right.pop();
    }
}

void find_left()
{
    stack<int> left;
    for (int i = n - 1; i >= 0; i--)
    {
        while (!left.empty() && hi[left.top()] > hi[i])
        {
            L[left.top()] = i + 1;
            left.pop();
        }
        left.push(i);
    }
    while (!left.empty())
    {
        L[left.top()] = 0;
        left.pop();
    }
}

int main()
{
    while (scanf("%d", &n) && n != 0)
    {
        for (int i = 0; i < n; i++)
            scanf("%lld", &hi[i]);
        
        find_left();
        find_right();
        long long area = 0, max_area = 0;
        for (int i = 0; i < n; i++)
        {
            area = (R[i] - L[i] + 1) * hi[i];
            if (area > max_area)
                max_area = area;
        }
        
        cout << max_area << endl;
    }
    return 0;
}

B TT’s Magic Cat

1. 题目大意

Thanks to everyone’s help last week, TT finally got a cute cat. But what TT didn’t expect is that this is a magic cat.

One day, the magic cat decided to investigate TT’s ability by giving a problem to him. That is select n cities from the world map, and a[I] represents the asset value owned by the i-th city.
Then the magic cat will perform several operations. Each turn is to choose the city in the interval [l,r] and increase their asset value by c. And finally, it is required to give the asset value of each city after q operations.

Could you help TT find the answer?

Input:
The first line contains two integers n,q (1≤n,q≤2·105) — the number of cities and operations.
The second line contains elements of the sequence a: integer numbers a1,a2,…,an (−106≤ai≤106).
Then q lines follow, each line represents an operation. The i-th line contains three integers l,r and c (1≤l≤r≤n,−105≤c≤ 105) for the i-th operation.

Output:
Print n integers a1,a2,…,an one per line, and ai should be equal to the final asset value of the i-th city.

Example:

4 2
-3 6 8 4
4 4 -2
3 3 1
-3 6 9 2

2. 思路历程

  • 简化题意为,每次对数组[l, r]区间中的所有数都进行+c操作,求出q次操作后的数组。

  • 可以暴力求解,对每个操作都遍历一次数组,给[l, r]区间的所有数都+c,但是复杂度达到O(qn),不能接受。

  • 此处引入差分数组

  1. 原数组A,差分数组B,数组范围[1, n]
  2. B[1] = A[1]B[i] = A[i] - A[i - 1]
  3. 特点:
    1)B数组的前缀和 <=> A数组的元素值 — — B[1] + B[2] + ... + B[i] = A[i]
    2)A数组的区间加 <=> B数组的单点修改 — — A[l] ~ A[R]均加上c <=> B[l] += c,B[R+1] -=c
  • 为降低复杂度,可以将原数组A转变为差分数组B,将区间修改转变为单点修改,q次操作后进行一次累加,计算出最终的A数组。

3. 具体实现

  • 创建数组a和b分别用于存放原数组和差分数组,输入a并构造b。
  • 对于每个操作q,只改变b[l-1]b[r]两个元素。(数组范围[0, n-1]
  • 遍历b数组,对其进行累加得到最终的a数组。

4. 代码

#include <iostream>
using namespace std;

int n, q, l, r, c;
long long sum, a[200022], b[200022];

int main()
{
    cin >> n >> q;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
        if (i) b[i] = a[i] - a[i-1];
        else b[i] = a[i];
    }
    
    for (int i = 0; i < q; i++)
    {
        cin >> l >> r >> c;
        b[l-1] += c;
        b[r] -= c;
    }
    
    sum = 0;
    for (int i = 0; i < n; i++)
    {
        sum += b[i];
        cout << sum << " ";
    }
    cout << endl;
    
    return 0;
}

C 平衡字符串

1. 题目大意

一个长度为 n 的字符串 s,其中仅包含 ‘Q’, ‘W’, ‘E’, ‘R’ 四种字符。
如果四种字符在字符串中出现次数均为 n/4,则其为一个平衡字符串。
现可以将 s 中连续的一段子串替换成相同长度的只包含那四个字符的任意字符串,使其变为一个平衡字符串,问替换子串的最小长度?
如果 s 已经平衡则输出0。

输入:
一行字符表示给定的字符串s。

输出:
一个整数表示答案。

样例:

QQWE
1

2. 思路历程

  • 要求出替换子串的最小长度,应从初始替换子串开始,根据其以外四种字符的个数,更新替换子串,最终得到最小值。
  • 可以发现最求答案是一个区间,更新时左右端点的移动有明确的方向,因此可以用尺取法。
  • 此处引入尺取法
  1. 采用双指针结构,在遍历数组过程中,两个指针向相同方向进行扫描。
  2. 适用的情况:所求解答案为一个连续区间,区间左右端点移动有明确的方向。
  • 运用尺取法确定左右端点
  1. 若当前[l, r]满足要求,则为了求最小区间,需要l++
  2. 若当前[l, r]不满足要求,则为了求满足的区间,需要r++

3. 具体实现

  • 创建sumQ, sumW, sumE, sumR分别用于记录除替换子串外的字符中四种字符的个数
    l和r分别用于记录替换字符串的左端点和右端点(初始l和r都是0)
    ans用于记录替换子串长度(初始取最大值便于之后比较取最小值)。
  • 输入字符串并对计算sum
  1. 若四个sum已相同,表明已满足,输出0并直接返回
  2. 若四个sum不相同,先将r往右移动一位,当r<n即未移动到字符串末尾时进行循环
    1)找到最大的sum,并通过max-sum计算出该情况下需要替换的字符个数
    2)若需要替换的字符大于替换字符串,说明该区间满足要求,则l++缩小区间再考察
    3)否则说明该区间不符合要求,r++加大区间再考察

4. 代码

#include <iostream>
#include <string.h>
using namespace std;

int l = 0, r = 0, ans = INT_MAX, sumQ = 0, sumW = 0, sumE = 0, sumR = 0;
long n;

int main()
{
    string s;
    cin >> s;
    n = s.length();
    for (int i = 0; i < n; i++)
    {
        if (s[i] == 'Q') sumQ++;
        if (s[i] == 'W') sumW++;
        if (s[i] == 'E') sumE++;
        if (s[i] == 'R') sumR++;
    }
    
    if (sumQ == sumW  && sumW == sumE && sumE == sumR)
    {
        cout << "0";
        return 0;
    }
    
    if (s[r] == 'Q') sumQ--;
    if (s[r] == 'W') sumW--;
    if (s[r] == 'E') sumE--;
    if (s[r] == 'R') sumR--;
    
    while (r < n)
    {
        int maxx = max(max(sumQ, sumW), max(sumE, sumR));
        int total = r - l + 1;
        int free = total - (maxx - sumQ) - (maxx - sumW) - (maxx - sumE) - (maxx - sumR);
        if (free >= 0)
        {
            ans = min(ans, total);
            if (s[l] == 'Q') sumQ++;
            if (s[l] == 'W') sumW++;
            if (s[l] == 'E') sumE++;
            if (s[l] == 'R') sumR++;
            if (l == r)
            {
                l++;
                r++;
            }
            else l++;
        }
        else
        {
            r++;
            if (s[r] == 'Q') sumQ--;
            if (s[r] == 'W') sumW--;
            if (s[r] == 'E') sumE--;
            if (s[r] == 'R') sumR--;
        }
    }
    cout << ans;
    
    return 0;
}

D 滑动窗口

1. 题目大意

ZJM 有一个长度为 n 的数列和一个大小为 k 的窗口, 窗口可以在数列上来回移动. 现在 ZJM 想知道在窗口从左往右滑的时候,每次窗口内数的最大值和最小值分别是多少. 例如:数列是 [1 3 -1 -3 5 3 6 7], 其中 k 等于 3.
在这里插入图片描述

输入:
输入有两行。第一行两个整数n和k分别表示数列的长度和滑动窗口的大小,1<=k<=n<=1000000。第二行有n个整数表示ZJM的数列。

输出:
输出有两行。第一行输出滑动窗口在从左到右的每个位置时,滑动窗口中的最小值。第二行是最大值。

样例:

8 3
1 3 -1 -3 5 3 6 7
-1 -3 -3 -3 3 3
3 3 5 5 6 7

2. 思路历程

  • 要求出一个窗口内的最大值和最小值,应在遍历数组时以窗口大小为单位进行考虑,是局部求最值的思想。

  • 移动窗口的方向为从左往右,因此遍历到元素 i 时确定[i - k + 1, i]区间(即 i 及其前k个元素)中的最大/小值。

  • 由于窗口移动会改变左右两个端点,且找到最值需要维护一定的单调性,因此考虑单调队列。

  • 此处引入单调队列这一数据结构

  1. 单调队列即维护了单调性的队列,满足队列的性质,队列中元素自队首到队尾满足单调性
  2. 在进入队列过程中就维护队列单调性,单调队列可以在队尾进行插入和弹出(与普通队列不同)
    如递增队列入队时,若队列为空或者队首元素大于入队元素可入队,否则需将不满足条件的队尾元素全部弹出后才可入队
  3. 与单调栈的区别:
    1)单调栈只维护一端(栈顶),而单调队列可以维护两端(队首和队尾)
    2)单调栈通常维护全局的单调性,而单调队列通常维护局部的单调性
    3)单调栈大小没有上限,而单调队列通常有大小限制
  • 维护一个单调递增队列,队列中的元素均属于当前窗口,当元素不属于当前窗口时,将队首元素弹出。

3. 具体实现

  • 创建四个数组分别为原数组、单调队列、最大值数组和最小值数组。
  • 两次遍历数组分别找到窗口的最大值和最小值
  1. 求最小值时维护递增队列,即元素i小于队尾元素时不满足单调性,需弹出队尾元素r--再入队
  2. 同时维护窗口的大小,若队首和队尾元素间隔超过窗口长度,则表明元素已不属于当前窗口,需移动窗口l++
  3. 确定当前窗口最小值的下标即为q[l]
  4. 求最大值时维护递减队列,其他操作完全相同
  • 最终只输出n - 1 + k个窗口的最大/小值(第0、1个元素不算)

4. 代码

#include <iostream>
using namespace std;

int n, k;
int a[1000011], q[1000011], minn[1000011], maxx[1000011];


int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++)
        scanf("%d", &a[i]);
    
    int l = 1, r = 0;
    for (int i = 0; i < n; i++)
    {
        while (l <= r && a[q[r]] >= a[i]) r--;
        q[++r] = i;
        if (q[r] - q[l] + 1 > k) l++;
        minn[i] = a[q[l]];
    }
    
    l = 0;
    r = 0;
    for (int i = 0; i < n; i++)
    {
        while (l <= r && a[q[r]] <= a[i]) r--;
        q[++r] = i;
        if (q[r] - q[l] + 1 > k) l++;
        maxx[i] = a[q[l]];
    }
    
    for (int i = k - 1; i < n; i++)
        cout << minn[i] << " ";
    cout << endl;
    for (int i = k - 1; i < n; i++)
        cout << maxx[i] << " ";
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值