喵
CONTENT
遺珠之憾…
問題:
給你
query x y
: 查詢序列 sqrt x y
: 把 問題:
給你
query x y
: 查詢序列 sqrt x y
: 把 先來看看一個問題:
給定一張
現在有
每次操作完後請輸出當前的連通塊數量。
當發現一個問題太難的時候,我們往往可以從較簡單的版本著手,試著找出解決難題的方向。
把兩種操作分開來看看吧!
給定一張
現在有
每次操作完後請輸出當前的連通塊數量。
並查集可以…
給定一張
現在有
每次操作完後請輸出當前的連通塊數量。
似乎沒有現成的資料結構可以維護(?
當題目沒要求強制在線時,往往有簡單的離線做法!
把所有操作先存起來
對於最終的Graph,如果把操作到回去做?
變成只有加邊的問題了!
給定一張
現在有
每次操作完後請輸出當前的連通塊數量。
時間線段樹,顧名思義就是將線段樹的每個節點當成對應的時間區間,並存放著該時間區間對應的所有>
操作,即在這邊操作是有時效性的。
使用上會把詢問與具有時效性的操作一起當成時間點,將這些時間點所形成的區間視為線段樹上的節點。
void insert_event(vector<Event> tr[], int idx,
int lb, int rb, int ql, int qr, Event e) {
// tr[] := 時間線段樹存事件所使用的陣列
// idx := 當前在時間線段樹上哪個節點
// 當前節點對應到的時間區間為 [lb, rb]
// 當前要在時間區間 [ql, qr] 插入一個事件e
// 當前區間 [lb, rb] 完全被 事件區間 [ql, qr] 包含
if (ql <= lb && rb <= qr) {
// 插入事件至時間線段樹對應的節點
tr[idx].push_back(e);
return;
}
int m = (lb + rb) / 2;
// 事件區間 [ql, qr] 完全位於 mid 左邊
if (qr <= mid)
insert_event(tr, idx*2, lb, mid, ql, qr, e);
// 事件區間 [ql, qr] 完全位於 mid 右邊
else if (mid < ql)
insert_event(tr, idx *2+1, mid+1, rb, ql, qr, e);
// 事件區間 [ql, qr] 跨過 mid 這個時間點
else {
insert_event(tr, idx*2, lb, mid, ql, qr, e);
insert_event(tr, idx*2+1, mid+1, rb, ql, qr, e);
}
}
void traversal(vector<Event> tr[], int idx, int lb, int rb)
{
int cnt = 0; // 用來記錄在這一層做了多少事情
for (auto e : tr[idx])
if (do_things(e))
cnt++;
if (lb == rb) // 記錄在這個時間點的答案
else {
int mid = (lb + rb) / 2;
// 遍歷時間線段樹的左子樹
traversal(tr, idx * 2, lb, mid);
// 遍歷時間線段樹的右子樹
traversal(tr, idx * 2 + 1, mid + 1, rb);
}
while (cnt--)
undo(); // 將所做的事情進行undo操作
}
如果對於每條邊,我們考慮它存活的時間…
int cc_cnt; // 當前連通塊數量
int sz[MAX_N]; // 用來記錄每個連通塊大小
int par[MAX_N]; // 用來記錄每個節點所指向的父節點
stack<pair<int*, int> > stk_sz; // 用來記錄每次操作前的sz
stack<pair<int*, int> > stk_par; // 用來記錄每次操作前的par
void init(int n) {
while (!stk_sz.empty())
stk_sz.pop();
while (!stk_par.empty())
stk_par.pop();
cc_cnt = n;
for (int i = 0; i < n; i++) {
par[i] = i;
sz[i] = 1;
}
}
不能路徑壓縮!
int find(int x) { // 找根
while (x != par[x])
x = par[x];
return x;
}
不能路經壓縮,所以一定要啟發式合併!
bool merge(int x, int y) {
x = find(x);
y = find(y);
if(x == y)
return 0;
if (sz[x] < sz[y]) // 啟發式合併
swap(x , y);
// 記錄操作前的sz
stk_sz.push(make_pair(&sz[x], sz[x]));
// 記錄操作前的par
stk_par.push(make_pair(&par[y], par[y]));
cc_cnt--;
sz[x] += sz[y];
par[y] = x;
return 1;
}
void undo() {
pair<int*, int> p_par = stk_par.back();
pair<int*, int> p_sz = stk_sz.back();
stk_par.pop();
stk_sz.pop();
*p_par.first = p_par.second; // 將par回復成操作前的par
*p_sz.first = p_sz.second; // 將sz回復成操作前的sz
cc_cnt++;
}
至此,對於可undo的並查集,每次merge操作的時間複雜度為
總時間複雜度為
總結一下,通常題目只要滿足以下兩個條件便可以考慮使用時間線段樹了:
給定一張
給你一個長度
緊接著
請你回答
A = {5, 2, 8, 1, 7}
Query = {(0, 2), (1, 4), (2, 2)}
答案:2 1 8
int ans = A[x];
for (int i = x + 1; i <= y; i++)
ans = min(ans, A[i]);
時間複雜度
特性:
可以對一部分數字先求最小值,再從每個部份的最小值中取最小的!
一部分?
每連續
對於詢問
綠色每塊都先算好了,時間花費是塊數:最多
紅色部份每個數字跑一遍,時間花費是一塊大小:最多
每次詢問
取個好的
算幾不等式:
在
所以取
1.大部分的區間詢問問題都需要用某種平衡二元搜尋樹來維護,若能用set,map,或是簡單的線段樹維護那還好,但如果性質很難維護或是要用到複雜的資料結構,寫起來會比較久也比較難debug。
2.分塊時跟陣列一樣只有一層,比較好想我們要維護什麼
3.code易懂
有
接下來有
(每張照片可以cover一個長度為
Greedy!
對於每一隻大象我們可以紀錄如果從他開始,我們要幾台相機才能蓋滿該塊的所有大象,且最後會蓋到哪個位置。
對某一塊加一隻大象,對某一塊刪除一隻大象
令一塊大小為
但悲劇的是每次操作後每一塊大小並不是固定的,
在經過
當有一塊過大時(可以定為
我們令
建表時間為
每次修改與查詢的時間複雜度為
重建表總時間為
給定一個序列
每次詢問
void add(int x) {
int now = ++app[x];
cnt[now - 1]--;
cnt[now]++;
curMax = max(curMax, now);
}
void sub(int x) {
int now = --app[x];
cnt[now + 1]--;
cnt[now]++;
if (!cnt[curMax])
curMax--;
}
for (int i = 1, curL = 1, curR = 0; i <= Q; i++) {
while (curR < qry[i].R) {
curR++;
add(S[curR]);
}
while (curR > qry[i].R) {
sub(S[curR]);
curR--;
}
while (curL > qry[i].L) {
curL--;
add(S[curL]);
}
while (curL < qry[i].L) {
sub(S[curL]);
curL++;
}
answer[qry[i].qid] = curMax;
}
左界屬於同一塊的詢問,每次都不會移動超過
而左界屬於不同塊的操作,移動次數加起來也不超過
所以左界移動的時間複雜度為
相同塊:因為
對於不同塊因為交界最多只有
取
遺珠QQ
長度
問每個人什麼時候達到目標
也就是擁有的元素加起來
每個人二分搜何時達到目標
用可持久線段樹維護每個操作做完的序列長相
時間
空間
MLE
mempry limit: 64 mb
開
看來不能每個人各自二分搜了
typedef vector< int > VI;
void totBS( int s , int e , VI nation ) {
if(s > e || nation.size() == 0)
return ;
int mid = (s + e) / 2;
do_things( s , mid );
VI ok, notok;
split( mid , nation , ok , notok );
undo_things( s , mid );
totBS( s , mid-1 , ok );
totBS( mid+1 , e , notok );
}
看那些人可以在 mid 之前達到目標
可以的丟去二分搜
不行的丟去二分搜
把 [s,mid] 的操作作用一番
BIT 維護之
void do_things( int s , int mid ) {
for( int i = s ; i <= mid ; i++ ) {
int nl = in[ i ].l , nr = in[ i ].r;
ll nnu = in[ i ].nu;
bit.upd(nl, nnu);
bit.upd(nr+1, -nnu);
}
}
看那些人在 mid 前可以被滿足
並且更新他們的目標 (把 [s,mid] 對他們貢獻扣掉)
void split( int mid , VI& nation , VI& ok , VI& notok ) {
for( int nat : nation ) {
tem[nat] = 0ll;
for( int it : satel[ nat ] ) {
tem[nat] += bit.qry(it);
if(tem[nat] >= need[nat])
break;
}
if(tem[nat] >= need[nat]) {
ok.push_back(nat);
if(ans[nat] > 0) ans[nat] = min(ans[nat], mid);
else ans[nat] = mid;
}
else {
need[nat] -= tem[nat];
notok.push_back(nat);
}
}
// 真的把 nation 佔的記憶體釋放掉
VI ().swap( nation );
}
把 BIT 還原
還給子孫乾淨的 BIT
void undo_things( int s , int mid ) {
for(int i = s; i <= mid; i++) {
int nl = in[i].l, nr = in[i].r;
ll nnu = in[i].nu;
bit.upd(nl, -nnu);
bit.upd(nr+1, nnu);
}
}
時間:
空間:
MLE
AC
其實這作法好好寫可以不用 BIT
時間少一個 O(logM)
空間也是好好線性
在線好難做
把操作們整理成方便處理的順序
變得好做
給你
計算下列 DP 式 :
阿不就斜率優化 DP
deque 做過去就沒啦
可是
慘
用平衡樹維護下凸包
算好一個
把
把附近斜率不必要的線拔掉
一堆邊界條件好難寫
想想分治
先把
按照
那就可以輕鬆做
所以可以得到長這樣的作法
void solve( int l , int r ) {
if( l == r ) return ;
int mid = ( l + r ) / 2;
solve( l , mid );
// 把 [l, mid] 整理好
// 算 [l, mid] 對 [mid+1, r] 答案的影響
solve( mid+1 , r );
}
整理好就只是
先把線做出來,排序好
vector< Line > lns , stk;
lns.clear();
for( int i = l ; i <= mid ; i++ )
if( in[ i ].dp >= in[ i ].P ) {
Ln l1{
in[ i ].G ,
in[ i ].dp-in[ i ].P+in[ i ].R
-( in[ i ].D+1ll )*in[ i ].G
};
lns.push_back( l1 );
}
sort( lns.begin() , lns.end() );
然後做出下凸包,把不必要的線拿走
stk.clear();
for( Ln l1 : lns ) {
while( stk.size()
&& stk.back().m == l1.m
&& stk.back().b < l1.b )
stk.pop_back();
if( stk.size() && stk.back().m == l1.m ) continue ;
while( stk.size() >= 2
&& !ok( stk[ stk.size() - 2 ] ,
stk[ stk.size() - 1 ] , l1 ) )
stk.pop_back();
stk.push_back( l1 );
}
像圖中紅色的線就是爛線
接下來就是要去更新
int j = 0;
for( int i = mid+1 ; i <= r ; i++ ) {
ll x = in[ i ].D;
while( j + 1 < (int)stk.size()
&& stk[ j ]( x ) < stk[ j+1 ]( x ) )
j++;
in[ i ].dp = max( in[ i ].dp , stk[ j ]( x ) );
}
dq( mid + 1 , r );
這裡只處理
好好 merge sort 線的斜率的話
時間 :
在一個二維平面上有
在ADA農場中有
貓咪如果上下左右移動的話,會耗費
貓咪如果走日字形移動的話,會耗費
定義
對於每隻貓咪
int calc(int dx, int dy) {
if (dx < dy) swap(dx, dy);
if (dx >= 2 * dy) return dx;
else return 2 * (dx + dy + 1) / 3;
}