题解:P1311 [NOIP2011 提高组] 选择客栈
Fun fact:本题题解区在清理前有
因为这道题是经典的一题多解题目,对计数子区间类的问题有重大的启发意义,所以在排除一些故弄玄虚故意复杂化问题的做法后,这篇题解将尽可能多地提供这个题题解区曾经有的不同做法。
那些题解有很多写得几乎使人无法阅读,我加上了自己的思考整理出这篇题解。
枚举咖啡店
时间复杂度
考虑枚举咖啡店,那么问题是同一种住宿方案可能有很多种咖啡店的选择方案,这样就会算重。于是我们不妨钦定一个可行的住宿方案在最靠左的咖啡店选择中被计算到。
假设我们枚举的
因为左边那个人的住宿必须满足这个咖啡店是最靠左的满足条件的咖啡店,右边那个人就没什么限制,只要保证咖啡店在他们中间就行了。
所以直接对所有颜色做前缀和即可。注意要减去两个人同时住在
int n,k,p;
int a[N],b[N];
int pre[N][51],suf[N][51];
int lst;
int main(){
ios::sync_with_stdio(false);
cin>>n>>k>>p;
fr1(i,1,n) cin>>a[i]>>b[i];
fr1(i,1,n){
if(b[i]<=p){
fr1(j,lst+1,i) pre[i][a[j]]++;
lst=i;
}
}
fr2(i,n,1){
fr1(j,0,k-1) suf[i][j]=suf[i+1][j];
suf[i][a[i]]++;
}
ll ans=0;
fr1(i,1,n){
if(b[i]<=p){
fr1(j,0,k-1) ans+=pre[i][j]*suf[i][j];
ans--;
}
}
cout<<ans<<'\n';
ET;
}
枚举右边的客栈
时间复杂度
考虑枚举右边的客栈,那么我们完全可以维护它及它前面第一个可行的咖啡店,也就是当前最靠右的合法咖啡店。很明显,这个咖啡店及其前面的客栈是可以供左边那个人随意选择的。
那么我们只需维护每种颜色在当前最靠右的合法咖啡店前面有多少种选择就可以了。可以在向右移动该咖啡店的同时维护掉。
注意也要去掉两个人同时住在咖啡店那个客栈的情况。
int n,k,p;
int a[N],b[N];
int sum[51];
int lst;
int main(){
ios::sync_with_stdio(false);
cin>>n>>k>>p;
fr1(i,1,n) cin>>a[i]>>b[i];
ll ans=0;
fr1(i,1,n){
if(b[i]<=p){
fr1(j,lst+1,i) sum[a[j]]++;
lst=i;
}
ans+=sum[a[i]]-(b[i]<=p);
}
cout<<ans<<'\n';
ET;
}
枚举左边的客栈(二分/双指针)
时间复杂度
其实也可以把上面枚举右边客栈的方法倒过来做,但是这里介绍另外一种更自然的想法。我们先按照颜色拆开,然后每种颜色独立做。
考虑对于某种颜色枚举左边的客栈,那么显然右边的可选客栈应该是单调的(前一半不合法,后一半合法)。因为只要存在一个合法咖啡店它右边的客栈就全部可选了。
所以找出第一个合法的客栈之后,这个客栈后面的全部同色客栈都可以计入答案。
考虑我们可以
于是二分加这个判定即可做到
也可以使用双指针。显然随着枚举的左边的客栈向右移动,第一个可选的右边的客栈一定也是向后移动的(区间内的咖啡店在越来越少)。那么我们只需要移动右边的客栈这个指针,维护区间是否合法即可。
于是可以做到
代码写的是双指针。
int n,k,p;
int a[N],b[N];
int nxt[N];
vector <int> col[55];
int main(){
#ifdef Shun
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
ios::sync_with_stdio(false);
cin>>n>>k>>p;
fr1(i,1,n) cin>>a[i]>>b[i];
nxt[n+1]=n+1;
fr2(i,n,1) {
if(b[i]<=p) nxt[i]=i;
else nxt[i]=nxt[i+1];
}
fr1(i,0,k-1) col[i].pb(0);
fr1(i,1,n) col[a[i]].pb(i);
ll ans=0;
fr1(i,0,k-1){
if(col[i].size()<=2) continue;
int m=col[i].size()-1;
int r=0;
fr1(l,1,m){
while(nxt[col[i][l]]>col[i][r]||l>r){
if(r>=m) break;
r++;
}
if(nxt[col[i][l]]<=col[i][r]) ans+=m-r+1-(l==r);
else break;
}
}
cout<<ans<<'\n';
ET;
}
Bonus:
其实也可以不用这么麻烦。考虑枚举左侧客栈
事实上可以发现
分治
时间复杂度
数合法子区间数!可以考虑分治。
我们考虑分治区间
考虑左半边
可以正向统计,但是不如把不合法的斥掉。可以发现不合法的方案就是左边的客栈选在了
注意每种颜色还是要分开统计,以及要处理一下某一半没有合法咖啡店的情况。
注意这里的代码我写成了
int n,k,p;
int a[N],b[N];
int lef[51],rig[51];
ll divide(int l,int r){
if(l==r) return 0;
ll ans=0;
int mid=(l+r>>1);
fr1(i,0,k-1) lef[i]=rig[i]=0;
fr1(i,l,mid) lef[a[i]]++;
fr1(i,mid+1,r) rig[a[i]]++;
fr1(i,0,k-1) ans+=lef[i]*rig[i];
int x=l-1;
fr2(i,mid,l){
if(b[i]<=p){
x=i;
break;
}
}
int y=r+1;
fr1(i,mid+1,r){
if(b[i]<=p){
y=i;
break;
}
}
fr1(i,0,k-1) lef[i]=rig[i]=0;
fr1(i,x+1,mid) lef[a[i]]++;
fr1(i,mid+1,y-1) rig[a[i]]++;
fr1(i,0,k-1) ans-=lef[i]*rig[i];
ans+=divide(l,mid);
ans+=divide(mid+1,r);
return ans;
}
int main(){
#ifdef Shun
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
ios::sync_with_stdio(false);
cin>>n>>k>>p;
fr1(i,1,n) cin>>a[i]>>b[i];
cout<<divide(1,n)<<'\n';
ET;
}
容斥
时间复杂度
一个比较厉害的做法。
考虑所有颜色可以产生的所有可能性是
然后我们考虑去掉其中不可行的客栈选法。这些客栈选法的共同点是不存在任何
所以对于每一段的每一种颜色又统计一次
实现精美一点就可以轻易做到时间和空间都线性了。
int n,k,p;
int a[N],b[N];
int sum[51];
ll ans;
ll C(int n){
if(n<2) return 0;
return 1ll*n*(n-1)/2;
}
stack <int> st;
int main(){
#ifdef Shun
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
ios::sync_with_stdio(false);
cin>>n>>k>>p;
fr1(i,1,n) cin>>a[i]>>b[i];
fr1(i,1,n) sum[a[i]]++;
fr1(i,0,k-1) ans+=C(sum[i]);
memset(sum,0,sizeof sum);
fr1(i,1,n){
if(b[i]>p) sum[a[i]]++,st.push(a[i]);
if(i==n||b[i]<=p){
while(!st.empty()){
ans-=C(sum[st.top()]);
sum[st.top()]=0;
st.pop();
}
}
}
cout<<ans<<'\n';
ET;
}
分块
时间复杂度
原题解里面有一个分块做法,看似是分块实则不是分块。
考虑分块怎么计数子区间。块内我们可以直接暴力枚举两端,在扫的同时维护区间最小值就可以了,两端合不合法直接看是否
考虑怎么统计跨块区间的贡献。考虑枚举两个块
所以这部分的时间复杂度
平衡取