javascript
洛谷P1198 [JSOI2008]最大数
P1198 [JSOI2008]最大數(shù)
-
- 267通過
- 1.2K提交
- 題目提供者該用戶不存在
- 標(biāo)簽線段樹各省省選
- 難度提高+/省選-
提交該題?討論?題解?記錄
最新討論
- WA80的戳這QwQ
- BZOJ都過了,洛谷竟然過不了…
- 為什么過不了
- = =我想說這題加優(yōu)讀會WA?…
- 誰說pascal只能80,要換c++…
- 線段樹為什么是80?
題目描述
現(xiàn)在請求你維護(hù)一個數(shù)列,要求提供以下兩種操作:
1、 查詢操作。
語法:Q L
功能:查詢當(dāng)前數(shù)列中末尾L個數(shù)中的最大的數(shù),并輸出這個數(shù)的值。
限制:L不超過當(dāng)前數(shù)列的長度。
2、 插入操作。
語法:A n
功能:將n加上t,其中t是最近一次查詢操作的答案(如果還未執(zhí)行過查詢操作,則t=0),并將所得結(jié)果對一個固定的常數(shù)D取模,將所得答案插入到數(shù)列的末尾。
限制:n是整數(shù)(可能為負(fù)數(shù))并且在長整范圍內(nèi)。
注意:初始時數(shù)列是空的,沒有一個數(shù)。
輸入輸出格式
輸入格式:?
第一行兩個整數(shù),M和D,其中M表示操作的個數(shù)(M <= 200,000),D如上文中所述,滿足(0<D<2,000,000,000)
接下來的M行,每行一個字符串,描述一個具體的操作。語法如上文所述。
?
輸出格式:?
對于每一個查詢操作,你應(yīng)該按照順序依次輸出結(jié)果,每個結(jié)果占一行。
?
輸入輸出樣例
輸入樣例#1:5 100 A 96 Q 1 A 97 Q 1 Q 2 輸出樣例#1:
96 93 96
說明
[JSOI2008]
分析:這道題有很多種辦法解決,首先可以發(fā)現(xiàn)數(shù)列中的數(shù)是遞增的,每次添加進(jìn)去的數(shù)都比之前的大,那么根據(jù)這個原理,模擬一下就能做出來.
這道題可以用來練線段樹,為什么想到要用線段樹呢?注意區(qū)間二字!在區(qū)間中查找最大值并且完成單點修改(插入),這不就是線段樹的最基本的操作嗎?因為線段樹可以全部賦值為-inf,所以插入操作可以理解為單點修改,套用線段樹的模板即可解決.
變量開成了全局變量,查了一個晚上的錯......
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm>#define le l,mid,o * 2 #define re mid + 1,r,o * 2 + 1using namespace std;const int maxn = 200001;int m, d,maxo[maxn << 2],len,t;void build(int l,int r,int o) {if (l == r){maxo[o] = -2147283647;return;}int mid = (l + r) >> 1;build(le);build(re); }void charu(int l, int r, int o, int i, int j) {if (l == r) { maxo[o] = j;return; }int mid = (l + r) >> 1;if (i <= mid)charu(le, i, j);else charu(re, i, j);maxo[o] = max(maxo[o * 2], maxo[o * 2 + 1]); }int query(int l, int r, int o, int x, int y) {if (x <= l && r <= y)return maxo[o];int mid = (l + r) >> 1;int temp = -2147483647;if (x <= mid)temp = max(temp,query(le, x, y));if (y > mid)temp = max(temp, query(re, x, y));return temp; }int main() {scanf("%d%d", &m, &d);build(1, maxn, 1);for (int b = 1;b <= m;++b){char c;int i;cin >> c;scanf("%d", &i);if (c == 'A') { len++;charu(1, maxn, 1, len, (i + t) % d); }else { t = query(1, maxn, 1, len - i + 1, len);printf("%d\n", t); }}return 0; }?
如果你還不會線段樹,那么可以參考一下下面這段文字(之前寫的可能不是很好,望體諒,可能也有一些不正確的,只能當(dāng)作參考):
線段樹,這個萬能的樹。
線段,線段,說白了就是一個區(qū)間,線段樹主要的操作就是對區(qū)間進(jìn)行修改查詢,效率非常高,線段樹的用途非常廣,單點更新、區(qū)間更新、最值詢問、區(qū)間詢問,至于它具體能干哪些事取決于樹里所儲存的信息量。
?
這是一個線段樹的圖,這個圖只是能夠幫我們理解線段樹的大體形狀,并不能告訴我們更多信息,其實線段樹的更多功能都隱藏在每一個節(jié)點的信息背后,為了能夠更方便的做題,我們給線段樹的每一個節(jié)點標(biāo)上序號。我們從上到下,從左到右依次標(biāo)號,如果根節(jié)點的序號為k,那么它的左子樹節(jié)點的序號則為2k,右子樹節(jié)點的序號則為2k + 1,每一個序號都對應(yīng)著唯一一個節(jié)點,所以我們可以用一個數(shù)組tree來表示這個節(jié)點背后所隱藏的信息。這個數(shù)組究竟開多大呢?雖然在平常做題中我們不需要考慮的這么仔細(xì),但在一些內(nèi)存限制非常緊的題目中這些都是要注意的。如果區(qū)間范圍是[0,N-1],那么tree的大小M=2*N + 1,這個很好驗證。
我們先來考慮如何建樹,一般來說,只要到了葉子節(jié)點直接輸入就好了,但是我們怎么樣才能夠很快的到達(dá)葉子節(jié)點呢?遞歸!
int tree[2 * MAX_N + 1];
?
/*建立以k為根節(jié)點[L,H]為操作區(qū)間的線段樹*/
void built_tree(int k, int L, int H)
{
??? if (L == H){
??????? scanf("%d", &tree[k]);
??????? return;
??? }
??? built_tree(k << 1, L, (L + H) << 1);
??? built_tree(k << 1 | 1, (L + H) << 1 | 1, H);
}
如果L==H,證明當(dāng)前區(qū)間的長度為1,也就是此節(jié)點為葉節(jié)點,可以直接賦值。
再來考慮一個經(jīng)典問題:求一個區(qū)間內(nèi)的最小元素值。
這道題可以用暴力來做,不過復(fù)雜度太高,在一些題目中可能會TLE,我們可以看到區(qū)間二字,那么這道題80%要用線段樹來做(當(dāng)然也不是絕對,只是效率高),我們不斷比較當(dāng)前查詢區(qū)間和目標(biāo)區(qū)間,如果當(dāng)前查詢區(qū)間在目標(biāo)區(qū)間內(nèi),那么當(dāng)前深度所表示的節(jié)點便可以參與最小值計算,如果不在區(qū)間內(nèi),則返回?zé)o窮大,否則則分別對當(dāng)前樹的左右子樹進(jìn)行相同運算(可能術(shù)語話太強了).
int read_tree(int k, int L, int H, int beg, int end)
{
??? if (beg > H || end < L) return -INT_MAX;
??? if (beg <= L && end >= H) return tree[k];
??? return min(read_tree(2 * k, L, (L + H) / 2, beg, end),
??????? read_tree(2 * k + 1, (L + H) / 2 + 1, H, beg, end));
}
有查詢,就一定伴隨著修改的存在,如果是普通的數(shù)組,修改很容易,只需要對所需要操作的下標(biāo)所對應(yīng)的數(shù)據(jù)修改即可,但是這是高效率數(shù)據(jù)結(jié)構(gòu),修改就意味著要對許多量進(jìn)行改變,在線段樹中,我們對一個節(jié)點進(jìn)行修改只需要對其及其所有的祖先進(jìn)行修改即可,其他量不變。
/*在根節(jié)點為k,[L,H]為操作區(qū)間的線段樹里對id處的值更新為key*/
int update_tree(int k, int L, int H, int id, int key)
{
??? if (L == H){
??????? tree[k] = key;
??????? return;
??? }
??? if (id < (L + H) / 2)
??????? update(k * 2, L, (L + H) / 2, id, key);
??? else
??????? update(K * 2 + 1, (L + H) / 2 + 1, H, id, key);
??? tree[k] = MAX(tree[k * 2], tree[2 * k + 1]);
}
這樣便完成了修改操作.
然后是比較復(fù)雜的區(qū)間修改,設(shè)計一個數(shù)據(jù)結(jié)構(gòu),使它支持兩種操作
這里要維護(hù)三個查詢值,該怎么維護(hù)呢?
首先這里的Add操作是區(qū)間修改,并不是單點修改,最糟糕的情況下可能整棵線段樹的結(jié)點值都要被修改。我們知道線段樹任意區(qū)間都能分解成不超過2h個不相交區(qū)間的并,利用這個結(jié)論我們可以將每一個Add操作分解成不超過2h個的Add操作,記錄在線段樹的結(jié)點中。每次執(zhí)行完Add操作都要重新計算每個結(jié)點的附加信息,遞歸訪問到的結(jié)點全部都要重新計算,并且是在遞歸返回后計算!
下面給出計算的代碼:
void weihu(int o,int L,int R)
{
??? int lc = o * 2, rc = o * 2 + 1;
??? sumv[o] = minv[o] = maxv[o] = 0;
??? if (R > L) {
??????? sumv[o] = sumv[lc] + sumv[rc];
??????? minv[o] = min(minv[lc], minv[rc]);
??????? maxv[o] = max(maxv[lc], maxv[rc]);
??? }
??? minv[o] += addv[o];
??? maxv[o] += addv[o];
??? sumv[o] += addv[o] * (R - L + 1);
}
對于下面的代碼來說,修改/查詢的范圍均為[y1,y2].
這里的sumv數(shù)組要說一下,為什么要用左右子結(jié)點相加得出呢?首先父親結(jié)點就包含了左右結(jié)點,其次這樣維護(hù)的時候就不需要修改全部的sumv數(shù)組的元素了。當(dāng)然這里指的是特殊情況,一般是那種極端數(shù)據(jù)的。
下面是Add操作的代碼:
void Add(int o, int L, int R)
{
??? int lc = o * 2, rc = o * 2 + 1;
??? if (y1 <= L && y2 >= R)
??????? addv[o] += v;
??? else {
??????? int M = (L + R) >> 1;
??????? if (y1 <= M)
??????????? Add(lc, L, M);
??????? if (y2 > M)
??????????? Add(rc, M + 1, R);
??? }
??? weihu(o, L, R);
}
其中addv數(shù)組是累加邊界的add值,因為一棵線段樹的子節(jié)點可能不知被修改一次,所以有必要設(shè)立這個數(shù)組。
然后是查詢操作,話說用線段樹步步都得謹(jǐn)慎,感覺這句話沒錯啊,每次進(jìn)行操作都要考慮到結(jié)點對結(jié)點之間有沒有影響,我們查詢一般都是從上往下遞歸查詢,既然一個結(jié)點的父結(jié)點執(zhí)行了add操作,而這個節(jié)點也被父節(jié)點包括在內(nèi),所以這個節(jié)點的值肯定被改變了,于是我們只能設(shè)3個全局變量來維護(hù)。
int _min, _max, _sum;
void query(int o, int L, int R, int add)
{
??? if (y1 <= L && y2 >= R) {
??????? _sum += sumv[o] + add * (R - L + 1); \
??????????? _min = min(_min, minv[o] + add);
??????? _max = max(_max, maxv[o] + add);
??? }
??? else {
??????? int M = (L + R) >> 1;
??????? if (y1 <= M)
??????????? query(o * 2, L, M, add + addv[o]);
??????? if (y2 > M)
??????????? query(o * 2 + 1, M + 1, R, add + addv[o]);
??? }
}
看到很多人都弄混了,好吧,其實我也有點暈了。可能會有人問了,為什么我們的weihu函數(shù)已經(jīng)維護(hù)了現(xiàn)在還要維護(hù)呢?因為weihu函數(shù)是從下到上的,也就是從左右子節(jié)點維護(hù)的,是相對于子節(jié)點所發(fā)生的變化,而這里的全局變量是因為父節(jié)點進(jìn)行了Add操作,子節(jié)點包含在內(nèi),所以要另開變量維護(hù)。如果還是搞不明白,可以看到weihu函數(shù)最后只是修改了當(dāng)前節(jié)點的值,并沒有維護(hù)到它的子節(jié)點,所以要另開變量維護(hù)。
接下來是更加復(fù)雜的:
Set(L,R,v)把AL,AL+1...AR的值全部修改為v.
Query(L,R)計算子序列AL,AL+1...AR的三個值(同上題).
可以看到這里變動的是Set操作,我們說這道題比之前復(fù)雜,為什么呢?因為之前的Add操作不管操作次序如何,都可以達(dá)到最后的結(jié)果,前提是算法是對的,代碼沒寫錯。然而Set操作則不同,好比刷油漆,最后刷的就是最終顏色。怎么辦呢?打標(biāo)記!這里的打標(biāo)記則相當(dāng)于對于被改變的特殊情況而做的變動,是為了最后的求出三個值而打的,那么怎么做呢?如果當(dāng)前區(qū)間完全被包含在我們需要修改/查詢的區(qū)間內(nèi),則直接修改標(biāo)記為v,否則則標(biāo)記下傳。
void pushdown(int o)
{
??? int lc = o * 2, rc = o * 2 + 1;
??? if (setv[o] >= 0)
??? {
??????? setv[lc] = setv[rc] = setv[o];
??????? setv[o] = -1;
??? }
}
這里的setv數(shù)組即為標(biāo)記,注意到這個數(shù)組被初始化為-1,這里不要搞錯了,那么問題來了:為什么我們要清除父節(jié)點的標(biāo)記呢?
接下來,Set操作代碼:
void Set(int o, int L, int R)
{
??? int lc = o * 2, rc = o * 2 + 1;
??? if (y1 <= L && y2 >= R)
??? {
??????? setv[o] = v;
??? }
??? else {
??????? pushdown(o);
??????? int M = (L + R) >> 1;
??????? if (y1 <= M)
??????????? Set(lc, L, M);
??????? else
??????????? maintain(lc, L, M);
??????? if (y2 > M)
??????????? Set(rc, M + 1, R);
??????? else
??????????? maintain(rc, M + 1, R);
??? }
??? maintain(o, L, R);
}
注意到3次maintain,最后一次很好理解,因為我們之前講過,每一次遞歸完后都必須要維護(hù)一次,那么前兩次又是為何呢?因為標(biāo)記一旦下傳,則該子樹的附加信息需要改變,當(dāng)前區(qū)間內(nèi)的子樹在遞歸完后自然會進(jìn)行維護(hù),不過另一個區(qū)間內(nèi)的子樹則沒有被維護(hù),因此需要加上兩次maintain函數(shù)的調(diào)用。
接下來是query操作的代碼:
void query(int o, int L, int R)
{
??? if (setv[o] >= 0) {
??????? _sum += setv[o] * (min(R, y2) - max(L, y1) + 1);
??????? _min = min(_min, setv[o]);
??????? _max = max(_max, setv[o]);
??? }
??? else if (y1 <= L && y2 >= R)
??? {
??????? _sum += sumv[o];
??????? _min = min(_min, minv[o]);
??????? _max = max(_max, maxv[o]);
??? }
??? else {
??????? int M = (L + R) >> 1;
??????? if (y1 <= M)
??????????? query(o * 2, L, M);
??????? if (y2 > M)
??????????? query(o * 2 + 1, M + 1, R);
??? }
}
對于有標(biāo)記的區(qū)間,我們要優(yōu)先處理,首先知道當(dāng)前區(qū)間都被修改為setv[o],既然所有值都是一樣的,自然就是對其進(jìn)行操作,然后再考慮被所要查詢的區(qū)間所完全包圍。回到之前的問題上來,為什么我們要清除父節(jié)點的標(biāo)記呢?我們將標(biāo)記下傳一般都是傳到被所要查詢的區(qū)間所完全包圍的區(qū)間,因為子節(jié)點的區(qū)間內(nèi)的值就包含了大區(qū)間的值,換句話說,所求的結(jié)果就是幾個小區(qū)間的并,而這幾個小區(qū)間則是分解到不能再分解為止,自然,我們將父節(jié)點的標(biāo)記消除因為子節(jié)點才是影響到結(jié)果的根本,我們求的值最終也在子節(jié)點進(jìn)行,所以要消除.
?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
轉(zhuǎn)載于:https://www.cnblogs.com/zbtrs/p/5851173.html
總結(jié)
以上是生活随笔為你收集整理的洛谷P1198 [JSOI2008]最大数的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第5章-css选择器初级和背景
- 下一篇: 手动实现JSON.stringify