P4062 [Code+#1]Yazid 的新生舞會
[Code+#1] Yazid 的新生舞會
經典老題,洛谷原題連結:https://www.luogu.com.cn/problem/P4062。
線性做法!
由於出現次數嚴格大於 \(\dfrac{r-l+1}2\),所以一個區間最多被一個眾數貢獻。因此考慮求出每個數作為符合題意的眾數出現的區間個數之和即答案。
我們設序列 \(s_k\) 表示 \(k\) 的出現次數字首和,即 \(s_{k,i}=\sum_{j=1}^i[a_j=k]\)。根據題意,一個區間 \([l+1,r]\) 被 \(k\) 貢獻當且僅當 \(s_{k,r}-s_{k,l}>\dfrac{r-l}2\)。稍作變形得 \(2s_{k,r}-r>2s_{k,l}-l\)
但是我們不能對每個 \(k\) 都 \(n\log n\) 求一遍逆序對。注意到出現次數總數為 \(n\),也就是說對於固定的 \(k\) 可能有一大串 \(s_k\)(\(s_{k,p}\sim s_{k,q}\))都相同,注意到計算貢獻其實就是詢問 \(q-p+1\) 段字首和,且右端點遞減,即矩形 + 等腰直角三角形。這個顯然可以線段樹維護區間 \(\sum v_i\)(矩形)以及 \(\sum i\times v_i\)(用梯形減去矩形就得到了等腰直角三角形)。線上段樹中加入這些值(\(s_{k,p}-p\sim s_{k,q}-q\)
此外清空不能直接 memset 否則複雜度會退化為 \(n^2\),需要記錄做過的操作然後撤銷。時間複雜度 \(\mathcal{O}(n\log n)\)。
用線段樹被卡常了怎麼辦?還有樹狀陣列。注意到一次修改實際上是在差分陣列上區間加,即對於每個 \(i\in [p,q]\),都給 \(s_{k,p}-i\sim \mathrm{upperbound}\) 加上 \(1\),一次修改是差分陣列單點加,兩次就是區間加。考慮維護差分陣列的差分陣列,那麼區間求和就是求三次字首和:我們維護的東西的字首和是差分陣列,差分陣列的字首和是原陣列,原陣列的字首和就是我們要單點求的東西。
對於這種樹狀陣列維護高階字首和
const int N = 5e5 + 5;
ll n, type, ans;
vint buc[N];
struct BIT {
ll a0[N << 1], a1[N << 1], a2[N << 1];
void add(ll x, int v) {
ll c0 = (x * x - 3 * x + 2) * v, c1 = (3 - 2 * x) * v;
while(x <= n * 2 + 1) a0[x] += c0, a1[x] += c1, a2[x] += v, x += x & -x;
}
void add(int l, int r, int v) {add(l + n + 1, v), add(r + n + 2, -v);}
ll query(int x) {
ll t = x, c0 = 0, c1 = 0, c2 = 0;
while(x) c0 += a0[x], c1 += a1[x], c2 += a2[x], x -= x & -x;
return c0 + c1 * t + c2 * t * t;
}
ll query(int l, int r) {return query(r + n + 1) - query(l + n);}
} tr;
vpii d;
void doit(int l, int r) {tr.add(l, r, 1), d.pb(l, r);}
void discard() {for(pii it : d) tr.add(it.fi, it.se, -1); d.clear();}
void solve(vint &cur) {
for(int i = 1; i < cur.size(); i++) {
int pre = cur[i - 1], now = cur[i], s = i - 1 << 1;
if(pre + 1 < now) {
int l = s - now + 1, r = s - pre - 1;
if(i > 1) ans += tr.query(l - 1, r - 1);
doit(l, r);
}
if(i < cur.size() - 1) {
int p = i * 2 - now;
ans += tr.query(p - 1, p - 1), doit(p, p);
}
}
discard();
}
int main(){
cin >> n >> type;
for(int i = 0; i < n; i++) buc[i].pb(0);
for(int i = 1; i <= n; i++) buc[read()].pb(i);
tr.add(0, 0, 1);
for(int i = 0; i < n; i++) buc[i].pb(n + 1), solve(buc[i]);
cout << ans / 2 << endl;
return 0;
}
能不能再給力一點啊?可以。
不妨設 \(d_i\) 表示 \(2s_{k,i}-i\)。
observation 1:對於固定的眾數 \(k\),最多有 \(2s_{k,n}\) 個位置對答案有貢獻。即對於所有 \(k\),使得 \(d_p>\min_{i=1}^{p-1}d_i\) 的 \(p\) 個數之和是 \(\mathcal{O}(n)\) 的。感性理解即若出現的 \(0\ (a_i\neq k)\) 足夠多,無論左端點怎麼往左擴充套件都無法使 \(k\) 符合條件。
上述結論使我們不需要求原陣列的區間和,只需要對於連續遞減的一段 \(d_i\),查詢單點即求出二階差分陣列的二階字首和,直到當前的 \(d_i\) 小於 \(d_i\) 的字首最小值,因為再列舉下去對答案的貢獻為 \(0\)(注意到我們是給 \([d_i+1,\infty]\) 區間加 \(1\) ),直接忽略。可以顯著減小常數。但問題仍不弱於求逆序對個數。
observation 2:\(|d_i-d_{i-1}|\leq 1\)。
上述結論保證了每次查詢的間隔不超過 \(1\),除了對於連續遞減的一段 \(d_{l\sim r}\) 的最後一次查詢(不妨設為位置 \(p\))與 \(d_{r+1}\) 之間會有一個間隔,但是忽略這個間隔這並不影響結果,因為 \(d_{r+1}-1\sim d_p-1\) 這一段並沒有進行任何修改:它小於 \(d_l\) 的字首最小值,自然不會產生任何貢獻。那麼由於指標的移動距離之和是 \(\mathcal{O}(n)\) 的,因此直接維護二階差分陣列,一階差分(基於二階差分陣列修改)和當前位置的值(基於一階差分修改)即可做到線性。
const int N = 5e5 + 5;
int n, type, add[N << 1], del[N << 1], s[N];
int cnt, hd[N], nxt[N << 1], val[N << 1];
void link(int u, int v) {nxt[++cnt] = hd[u], hd[u] = cnt, val[cnt] = v;}
void modify(int l, int r, int v) {add[l + N] += v, del[r + N + 1] += v;}
ll ans;
int main(){
cin >> n >> type, modify(1, 1, 1);
for(int i = 1; i <= n; i++) link(read(), i);
for(int p = 0; p < n; p++) {
if(!hd[p]) continue;
int minp = 0, id = 1;
static int tmp[N]; tmp[0] = 0, tmp[1] = n + 1;
for(int i = hd[p]; i; i = nxt[i]) tmp[++id] = val[i];
reverse(tmp + 1, tmp + id + 1);
ll dc = 0, c = 0;
for(int i = 1; i <= id; i++) {
int pre = tmp[i - 1], cur = tmp[i];
int r = s[i - 1] - 1, l = r - (cur - pre) + 2;
if(l <= r && pre) {
int lim = max(minp, l);
for(int j = r; j >= lim; j--) {
c -= dc, ans += c;
dc += del[j + N + 1] - add[j + N + 1];
}
}
if(i == id) break;
modify(l + 1, r + 1, 1);
dc += add[l + N + 1] - del[l + N + 1];
c += dc, ans += c, s[i] = l + 1;
modify(s[i] + 1, s[i] + 1, 1), cmin(minp, l);
}
for(int i = 1; i < id; i++)
modify(s[i], s[i - 1], -1),
modify(s[i] + 1, s[i] + 1, -1);
}
cout << ans << endl;
return cerr << clock() << endl, flush(), 0;
}