Codeforces 1175F - The Number of Subpermutations(線段樹+單調棧+雙針/分治+啟發式優化)
由於這場的 G 是道毒瘤題,蒟蒻切不動就只好來把這場的 F 水掉了
看到這樣的設問沒人想到這道題嗎?那我就來發篇線段樹+單調棧的做法。
首先顯然一個區間 \([l,r]\) 滿足條件當且僅當:
- \([l,r]\) 中不存在重複的數值
- \([l,r]\) 中最小值為 \(1\)
- \([l,r]\) 中最大值為 \(r-l+1\)
題解區中某位大佬說過:“數區間的題無非兩種套路,列舉端點和分治”,這裡咱們考慮列舉端點。具體來說,咱們列舉右端點 \(r\),那麼滿足 \([l,r]\) 中不存在重複的數值的 \(l\) 顯然組成了一段連續的區間,且這個區間的右端點就是 \(r\)
時間複雜度 \(\mathcal O(n\log n)\)
const int MAXN=3e5; int n,a[MAXN+5]; struct node{int l,r;pii p;ll lz;} s[MAXN*4+5]; pii operator +(pii lhs,pii rhs){ pii res;res.fi=min(lhs.fi,rhs.fi); if(res.fi==lhs.fi) res.se+=lhs.se; if(res.fi==rhs.fi) res.se+=rhs.se; return res; } void pushup(int k){s[k].p=s[k<<1].p+s[k<<1|1].p;} void build(int k,int l,int r){ s[k].l=l;s[k].r=r;if(l==r) return s[k].p=mp(0,1),void(); int mid=l+r>>1;build(k<<1,l,mid);build(k<<1|1,mid+1,r);pushup(k); } void pushdown(int k){ if(s[k].lz){ s[k<<1].p.fi+=s[k].lz;s[k<<1].lz+=s[k].lz; s[k<<1|1].p.fi+=s[k].lz;s[k<<1|1].lz+=s[k].lz; s[k].lz=0; } } void modify(int k,int l,int r,int x){ if(l<=s[k].l&&s[k].r<=r){ s[k].p.fi+=x;s[k].lz+=x;return; } pushdown(k);int mid=s[k].l+s[k].r>>1; if(r<=mid) modify(k<<1,l,r,x); else if(l>mid) modify(k<<1|1,l,r,x); else modify(k<<1,l,mid,x),modify(k<<1|1,mid+1,r,x); pushup(k); } pii query(int k,int l,int r){ if(l<=s[k].l&&s[k].r<=r) return s[k].p; pushdown(k);int mid=s[k].l+s[k].r>>1; if(r<=mid) return query(k<<1,l,r); else if(l>mid) return query(k<<1|1,l,r); else return query(k<<1,l,mid)+query(k<<1|1,mid+1,r); } int pre[MAXN+5],cnt[MAXN+5]; int main(){ scanf("%d",&n);build(1,1,n);ll res=0; for(int i=1;i<=n;i++) scanf("%d",&a[i]); stack<int> stk;stk.push(0);a[0]=0x3f3f3f3f; for(int i=1,j=1;i<=n;i++){ cnt[a[i]]++;while(cnt[a[i]]>=2) cnt[a[j++]]--; pre[a[i]]=i;modify(1,1,i,-1);modify(1,i,i,a[i]); while(!stk.empty()&&a[stk.top()]<a[i]){ int x=stk.top();stk.pop(); modify(1,stk.top()+1,x,a[i]-a[x]); } stk.push(i);if(pre[1]>=j){ pii p=query(1,j,pre[1]); if(!p.fi) res+=p.se; } } printf("%ld\n",res); return 0; }
還有一種思路便是分治(既然上面咱們選擇了列舉端點,那這邊咱們就要選擇分治咯)
我們首先考慮怎樣判斷一個區間是否存在相同元素,按照區間數顏色的套路,我們記 \(p_i\) 表示 \(i\) 前面上一個與 \(a_i\) 相等的 \(a_j\) 的位置,那麼區間 \([l,r]\) 不存在重複元素的充要條件是 \(\max\limits_{i=l}^rp_i<l\)。考慮分治,處理左右端點 \([l,r]\) 都在 \([l,r]\) 中的區間時,我們找出區間最大值所在的位置 \(p\),那麼顯然 \([l,r]\) 中的區間可以像點分治那樣分成三類:完全包含於 \([l,p-1]\)、完全包含於 \([p+1,r]\),以及跨過 \(p\),前兩類顯然可以遞迴處理。關於第三類,顯然區間中最大的數就是 \(a_p\),區間長度也就是 \(a_p\),因此我們列舉所有長度為 \(a_p\)、且跨過位置 \(p\)(這點一定要判斷)的區間計算貢獻即可,但這樣會 T,考慮優化,顯然區間左端點必須在 \([l,p]\) 中對吧,右端點必須在 \([p,r]\) 中對吧,那麼我們就考慮 \([l,p],[p,r]\) 中長度的較小者,如果 \([l,p]\) 長度較小就列舉左端點 \(L\in[l,p]\),否則列舉右端點 \(R\in[p,r]\)。這樣乍一看複雜度沒啥變化,不過按照這題的套路,這其實相當於啟發式合併的逆過程,即啟發式分裂(瞎起名字 ing),因此複雜度是嚴格單 log 的。
非常神奇,誰能告訴我為什麼兩個程式跑得一樣快……358 ms
const int MAXN=3e5;
const int LOG_N=18;
int n,a[MAXN+5],pre[MAXN+5],st[MAXN+5][LOG_N+2],res=0;
pii st_val[MAXN+5][LOG_N+2];
int query(int l,int r){
int k=31-__builtin_clz(r-l+1);
return max(st[l][k],st[r-(1<<k)+1][k]);
}
int query_ps(int l,int r){
int k=31-__builtin_clz(r-l+1);
return max(st_val[l][k],st_val[r-(1<<k)+1][k]).se;
}
void solve(int l,int r){
if(l>r) return;int ps=query_ps(l,r),len=a[ps];
solve(l,ps-1);solve(ps+1,r);
if(ps-l+1<=r-ps+1){
for(int i=l;i<=ps;i++) if(i+len-1<=r&&i+len-1>=ps&&query(i,i+len-1)<i)
res++;
} else {
for(int i=ps;i<=r;i++) if(i-len+1>=l&&i-len+1<=ps&&query(i-len+1,i)<i-len+1)
res++;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]),st_val[i][0]=mp(a[i],i);
for(int i=1;i<=n;i++) st[i][0]=pre[a[i]],pre[a[i]]=i;
for(int i=1;i<=LOG_N;i++) for(int j=1;j+(1<<i)-1<=n;j++){
st[j][i]=max(st[j][i-1],st[j+(1<<i-1)][i-1]);
st_val[j][i]=max(st_val[j][i-1],st_val[j+(1<<i-1)][i-1]);
} solve(1,n);printf("%d\n",res);
return 0;
}