1. 程式人生 > 其它 >HDU 1754 I Hate It

HDU 1754 I Hate It

題目傳送門

本題是單點修改,區間求極大極小值的模板題。

一、陣列的含義

1、在維護和查詢區間和的演算法中,\(t[x]\)中儲存的是\([x,x-lowbit(x)+1]\)中每個數的和。

2、在求區間最值的演算法中,\(t[x]\)儲存的是\([x,x-lowbit(x)+1]\)中所有數的最大值。

3、求區間最值的演算法中還有一個\(a[i]\)陣列,表示第\(i\)個數是多少。

二、單點修改引發的變化

一個位置被修改,原陣列肯定要修改,同時,作為儲存統計資訊的樹狀陣列也要進行一些修改:

void update(int x, int v) {
    while (x <= n) t[x] = max(t[x], v), x += lowbit(x);
}

理解一下:從\(x\)這個位置開始,把在範圍內(\(x<=n\)),比它腦袋頂上覆蓋的長條(\(t[x]\))都嘗試更新最大值。

三、區間查詢時的思路

求區間最大值 和 求區間和有很大的不同,因為樹狀陣列求字首和十分方便,所以通過 \(sum (R) - sum (L-1)\) 就能得到一個區間的和;那麼區間最大值怎麼求?

假設要求 \(L\)\(R\) 的區間最大值,我們分塊仍然是根據\(lowbit(x)\) 的方式分,對應分塊的最大值也很容易想到,就是求字首和中累加的部分改成 \(max ()\) 就行了;

查詢: 如果我們根據\(lowbit\) 的規則從右向左求區間的最大值,所求的結果是 \(1\)

\(R\) 這個區間的最大值,顯然答案可能是不正確的; 我們如何避開 \(1\)\(L-1\) 這個區間?

我們根據\(lowbit\)的規則,從右向左遍歷區間塊,如果是區間是我們要考慮的範圍內就取這個最大值,如果發現當前的區間超出我們要考慮的範圍就只取當前區間最末尾的值,然後\(lowbit - 1\),然後繼續從右向左遍歷,直到所有區間取完為止。

根據上圖的步驟,下標為\(12\)的這個位置是 \(9\)\(12\)的最大值,我們能通過\(lowbit\)知道當前區間的長度是多少,由此能知道當前區間是不是在我們要考慮的範圍內;當遇到下標為\(8\),區間長度大於當前我們要考慮的長度時,我們就只取最末尾的值,也就是對應陣列\(a[8]\)

\(lowbit - 1\) 到下標為\(7\)這個位置後繼續從右向左判斷,直到結束。 從圖中可以很容易發現,以這種方式遍歷,能把所有我們需要考慮的部分都遍歷了一遍。

四、程式碼模板

// http://acm.hdu.edu.cn/showproblem.php?pid=1754

// 待研讀: 樹狀陣列 資料結構詳解與模板(可能是最詳細的了)
// https://blog.csdn.net/bestsort/article/details/80796531

// 本題題解
// https://blog.csdn.net/mosquito_zm/article/details/76422738

// 求區間最大值
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <vector>
#include <map>
#include <queue>
#include <algorithm>
#include <math.h>
#include <cstdio>

using namespace std;
const int N = 200010;

// n個數,m個操作
int n, m;
//原始資料 / 樹狀陣列
int a[N], t[N];
//指令字串
char op[2];

int lowbit(int x) {
    return x & (-x);
}

//樹狀陣列維護最大值
//從i開始,一路將i+lowbit(i)的都嘗試更新最大值v
//樹狀陣列,底部一層是固定的,是按索引號來的
void update(int x, int v) {
    while (x <= n) t[x] = max(t[x], v), x += lowbit(x);
}
/*
用一個數組a來存放原始陣列,用陣列t來存樹狀陣列。
每次單點修改,都將受影響的a和t的值修改,時間複雜度為O(logn).
每次區間查詢[l,r]的最大值,如果區間長度大於lowbit(r)就一次性跨越一個lowbit(r)來查詢,
否則就一個點一個點的跨越。

把查詢過程看作砍柴:
砍一個點、砍一段區間(砍完至少剩一個點)、砍一段區間、砍一段區間…、
剩下的部分不足一段區間+一個點的時,一個點一個點的砍。
砍一個區間用t,砍一個點用a。
*/
//查詢區間[l,r]的最大值
int query(int x, int y) {
    int ans = 0;
    while (y >= x) {
        ans = max(ans, a[y--]);
        //以5~12為例
        // y = 12 -> 1100    x = 5-> 0101
        // y-lowbit(y) = 1000  t[12]描述的是在9~12之間的最大值
        // 不能再砍了,再砍就砍冒了~
        // 此時 y--,也就是y = 7,然後再一個一個點的砍~
        while (y - lowbit(y) >= x) ans = max(t[y], ans), y -= lowbit(y);
    }
    return ans;
}

int main() {
    // n個數,m個操作
    while (~scanf("%d %d", &n, &m)) {
        //清空樹狀陣列
        memset(t, 0, sizeof(t));
        //讀入n個數
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i]);
            //構建+更新樹狀陣列
            update(i, a[i]);
        }

        int x, y;
        while (m--) {
            scanf("%s%d%d", op, &x, &y);
            if (*op == 'U') { //更新操作,要求把ID為x的學生的成績更改為y
                a[x] = y;
                update(x, a[x]);
            } else if (*op == 'Q') //詢問ID從x到y(包括x,y)的學生當中,成績最高的是多少。
                printf("%d\n", query(x, y));
        }
    }
}