洛谷题单 算法2-3 分治与倍增
洛谷题单 算法2-3 分治与倍增
P2345 [USACO04OPEN] MooFest G 树状数组
思路:
让我们求所有两两之间 \(max(v_i,v_j)\times |x_i-x_j|\) 的值之和。
经典思路,考虑每个数对答案的贡献。对于每个数 \(v\),会产生所有比 \(v\) 小的数和它的距离之和倍的
\(v\) 的贡献。考虑如何求解这个距离之和。为了去掉绝对值,我们可以分开讨论,不妨设 \(x_i>x_j\)。
那么贡献就可以表示为:\(v_i\times(p\times x_i-sum)\)。其中 \(p\) 为所有小于 \(x_i\) 的数的个数,\(sum\) 为所有小于
\(x_i\) 的数之和。这两部分只需要开两个树状数组即可维护。对于 \(x_i<x_j\) 的情况,同理。
注意:如果存在两个值相同的情况,那么正着反着树状数组维护的时候就会重复计算,因此第二次计算的时候,我们只需要维护 \(v-1\) 即可。
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int N = 5e4+10;
void Showball(){
int n;
cin>>n;
vector<int> a(n),b(n),p(n);
for(int i=0;i<n;i++){
cin>>a[i]>>b[i];
}
iota(p.begin(),p.end(),0);
sort(p.begin(),p.end(),[&](int x,int y){
return b[x]<b[y];
});
vector<array<i64,2>> tr(N);
auto add=[&](int op,int x,int v){
for(;x<N;x+=x&-x) tr[x][op]+=v;
};
auto ask=[&](int op,int x){
i64 ret=0;
for(;x;x-=x&-x) ret+=tr[x][op];
return ret;
};
i64 ans=0;
for(int i=0;i<n;i++){
int v=a[p[i]],id=b[p[i]];
ans+=1LL*v*(ask(1,v)*id-ask(0,v));
add(0,v,id);
add(1,v,1);
}
tr.assign(tr.size(),{0,0});
for(int i=n-1;i>=0;i--){
int v=a[p[i]],id=b[p[i]];
ans+=1LL*v*(ask(0,v-1)-ask(1,v-1)*id);
add(0,v,id);
add(1,v,1);
}
cout<<ans<<"\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
P1966 [NOIP2013 提高组] 火柴排队 树状数组
首先对于最小值的情况。根据 排序不等式 , 两列都排序后一定是最优解。
那么我们就可以按照 \(a\) 的顺序给 \(b\) 排序即可(反过来也一样)。因为每次都只能交换
相邻的火柴(其实就是冒泡排序)。那么我们知道最少交换次数= 逆序对 的数量。
我们映射一下 \(a\) 和 \(b\) 数组相对关系,然后求逆序对即可。树状数组求解一下就好。
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int mod=1e8-3;
void Showball(){
int n;
cin>>n;
vector<int> a(n+1),b(n+1);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) cin>>b[i];
vector<i64> tr(n+1);
auto add=[&](int x,int v){
for(;x<=n;x+=x&-x) tr[x]+=v;
};
auto ask=[&](int x){
i64 ret=0;
for(;x;x-=x&-x) ret+=tr[x];
return ret;
};
vector<int> p(n+1),q(n+1);
iota(p.begin(),p.end(),0);
iota(q.begin(),q.end(),0);
sort(p.begin()+1,p.end(),[&](int x,int y){
return a[x]<a[y];
});
sort(q.begin()+1,q.end(),[&](int x,int y){
return b[x]<b[y];
});
vector<int> t(n+1);
for(int i=1;i<=n;i++){//t[i]表示a中排第i的数,b中排多少?
t[p[i]]=q[i];
}
i64 ans=0;
for(int i=n;i;i--){
ans=(ans+ask(t[i]-1))%mod;
add(t[i],1);
}
cout<<ans<<"\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
P7167 [eJOI2020 Day1] Fountain 单调栈+倍增
先用单调栈维护出每个圆盘下第一个比它直径大的圆盘。
然后倍增维护装水量之和即可。查询类比 \(LCA\)。
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const i64 inf=1e18;
void Showball(){
int n,q;
cin>>n>>q;
vector<int> d(n+2),c(n+2);
for(int i=1;i<=n;i++) cin>>d[i]>>c[i];
vector<array<i64,25>> f(n+2),g(n+2);
for(int i=1;i<=n+1;i++){
for(int j=0;j<=20;j++){
g[i][j]=inf;
}
}
//单调栈
stack<i64> stk;
for(int i=n;i;i--){
while(stk.size()&&d[stk.top()]<=d[i]) stk.pop();
if(stk.size()){
f[i][0]=stk.top();
g[i][0]=c[stk.top()];
}else{
f[i][0]=0;
}
stk.push(i);
}
//倍增
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i+(1<<j)<=n;i++){
f[i][j]=f[f[i][j-1]][j-1];
g[i][j]=g[i][j-1]+g[f[i][j-1]][j-1];
}
}
auto query=[&](int r,int v)->i64{
if(c[r]>=v) return r;
v-=c[r];
for(int i=20;i>=0;i--){
if(f[r][i]&&g[r][i]<v){
v-=g[r][i];
r=f[r][i];
}
}
return f[r][0];
};
while(q--){
int r,v;
cin>>r>>v;
cout<<query(r,v)<<"\n";
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
P3509 [POI2010] ZAB-Frog 单调队列+倍增
单调队列维护出每个点下一次跳到的点,然后倍增处理即可。由于跳的次数已经给定了,那么就可以用类似快速幂的写法去写了。
这个单调队列稍微特殊一点,因为距离该点的距离前 \(k\) 小的点一定位于
该点两边,因此我们需要维护区间长度不变,头和尾需要同时更新。
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
void Showball(){
i64 n,k,m;
cin>>n>>k>>m;
vector<i64> a(n+1);
for(int i=1;i<=n;i++) cin>>a[i];
vector<i64> f(n+1);
f[1]=k+1;
i64 l=1,r=k+1;
for(int i=2;i<=n;i++){
while(r+1<=n&&a[i]-a[l]>a[r+1]-a[i]) l++,r++;
if(a[i]-a[l]>=a[r]-a[i]) f[i]=l;
else f[i]=r;
}
vector<i64> ans(n+1);
iota(ans.begin(),ans.end(),0);
//倍增
while(m){
if(m&1){
for(int i=1;i<=n;i++) ans[i]=f[ans[i]];
}
auto nf=f;
for(int i=1;i<=n;i++) f[i]=nf[nf[i]];
m>>=1;
}
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
P4155 [SCOI2015] 国旗计划 双指针+倍增
首先,对于环上问题。经典的断环为链,变成线段上的问题。
因为线段不包含,按照左端点排序,右端点也是单调的。
那么我们先考虑如何求每个 \(i\) 的下一段线段。
即找出 \(j\) 满足 \(j=max(x|a_x.l\le a_i.r)\) 。那么这部分就可以用
双指针维护。
接着下来倍增即可。查询时倍增至 \(a_j.r<a_i.l+m\) 即可。
最后答案要加上 \(2\) ,即最后一段线段和第 \(i\) 条线段。
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
struct node{
int id,l,r;
bool operator<(const node &u)const{
return l<u.l;
}
};
void Showball(){
int n,m;
cin>>n>>m;
vector<node> a(n<<1|1);
for(int i=1;i<=n;i++){
int l,r;
cin>>l>>r;
if(l>r) r+=m;
a[i]=node{i,l,r};
}
sort(a.begin()+1,a.begin()+n+1);
for(int i=1;i<=n;i++){
a[i+n].l=a[i].l+m;
a[i+n].r=a[i].r+m;
}
//双指针
vector<array<int,25>> f(n<<1|1);
for(int i=1,j=1;i<=2*n;i++){
while(j<=2*n&&a[j].l<=a[i].r) j++;
f[i][0]=j-1;
}
//倍增
for(int j=1;(1<<j)<=2*n;j++){
for(int i=1;i+(1<<j)<=2*n;i++){
f[i][j]=f[f[i][j-1]][j-1];
}
}
auto query=[&](int id){
i64 ret=2;
int lim=a[id].l+m;
for(int i=20;i>=0;i--){
if(f[id][i]&&a[f[id][i]].r<lim){
ret+=(1<<i);
id=f[id][i];
}
}
return ret;
};
vector<int> ans(n+1);
for(int i=1;i<=n;i++){
ans[a[i].id]=query(i);
}
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
P6648 [CCC2019] Triangle: The Data Structure 单调队列+倍增
本质是一个二维的RMQ问题。
令 \(f_{i,j,k}\) 表示从 \((i,j)\) 开始大小为 \(2^k\) 的三角形内的最大值。
那么就可以去倍增维护。
空间会超,所以我们需要滚动优化一下。
发现会TLE一部分数据。因为时间复杂度暴了。
观察到中间,我们维护的是固定区间长度的最大值。
那么就可以单调队列进行优化。
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int N=3010;
int f[N][N][2];
int a[N][N],q[N];
void Showball(){
int n,k;
cin>>n>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin>>a[i][j];
f[i][j][0]=a[i][j];
}
}
for(int l=1;l<=log2(k);l++){
for(int i=1;i+(1<<l-1)<=n;i++){
int t=i+(1<<l-1);
int len=(1<<l-1)+1;
int hh=1,tt=0;
for(int j=1;j<=n;j++){
f[i][j][l%2]=f[i][j][(l-1)%2];
while(hh<=tt&&j-q[hh]+1>len) hh++;
while(hh<=tt&&f[t][j][(l-1)%2]>f[t][q[tt]][(l-1)%2]) tt--;
q[++tt]=j;
if(j>=len) f[i][j-len+1][l%2]=max(f[i][j-len+1][l%2],f[t][q[hh]][(l-1)%2]);
}
}
}
i64 ans=0;
for(int i=1;i+k-1<=n;i++){
for(int j=1;j<=i;j++){
int len=log2(k);
int maxn=f[i][j][len%2];
int t=i+k-(1<<len);
for(int u=j;u<=j+k-(1<<len);u++){
maxn=max(maxn,f[t][u][len%2]);
}
ans+=maxn;
}
}
cout<<ans<<"\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int ttt=1;
//cin>>t;
while(ttt--){
Showball();
}
return 0;
}
P7562 [JOISC 2021 Day4] イベント巡り 2 (Event Hopping 2) 思维+倍增
有 \(N\) 条线段,选 \(K\) 条,它们互不相交(但可以在端点相连)。输出字典序最小的选择方案。
因为要输出字典序最小的方案。考虑遍历每个区间,如果该区间能选就一定要选。
因此问题的关键为 如何判断一个区间能不能选
1.判断一个区间与之前已经选择的区间是否有冲突(交叉或者包含)
2.判断选择一个区间之后,剩下的区间可选数量能确保一共够 \(k\) 个
对于第一个问题:
我们可以用 \(set\) 来维护空闲的区间,定义结构体,并重载运算符。
struct node{
int l,r;
bool operator<(const node &u)const{
return r<u.l;
}
};
这样我们就可以用 find
函数查到与当前区间 [x,y]
相交的区间,然后再判断是否完全包含即可。包含的区间为 [l,r]
。如果包含,那么我们就在 set
中删除 [l,r]
,并且添加 [l,x]
和 [y,r]
这两个区间。
对于第二个问题:
我们用 calc(l,r)
表示在 [l,r]
区间中可选区间的最大数量。令 cur
表示当前可选区间数量。
那么每次我们需要去维护 cur
, cur=cur-calc(l,r)+calc(l,x)+calc(y,r)
。
然后判断剩余可选区间数量 + 已选区间数量是否 \(\geq k\) 即可。
那么如何去求 \(calc(l,r)\) 呢?倍增即可,具体参考代码。
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int N=1e5+10;
struct node{
int l,r;
bool operator<(const node &u)const{
return r<u.l;
}
};
int f[N<<1][25];
void Showball(){
int n,k;
cin>>n>>k;
vector<int> l(n+1),r(n+1),rk;
for(int i=1;i<=n;i++){
cin>>l[i]>>r[i];
rk.push_back(l[i]);
rk.push_back(r[i]);
}
//离散化
map<int,int> pos;
sort(rk.begin(),rk.end());
rk.erase(unique(rk.begin(),rk.end()),rk.end());
int m=rk.size();
for(int i=0;i<m;i++) pos[rk[i]]=i+1;
for(int i=1;i<=n;i++){
l[i]=pos[l[i]];
r[i]=pos[r[i]];
}
//倍增
memset(f,0x3f,sizeof f);
for(int i=1;i<=n;i++){
f[l[i]][0]=min(f[l[i]][0],r[i]);
}
for(int i=m;i>=1;i--){
f[i][0]=min(f[i][0],f[i+1][0]);
}
for(int j=1;(1<<j)<=n;j++){
for(int i=1;i<=m;i++){
if(f[i][j-1]<=m)
f[i][j]=f[f[i][j-1]][j-1];
}
}
auto calc=[&](int l,int r){
int u=l,ret=0;
for(int i=20;i>=0;i--){
if(f[u][i]<=r){
ret+=(1<<i);
u=f[u][i];
}
}
return ret;
};
set<node> st;
st.insert(node{1,m});
int cur=calc(1,m);
vector<int> ans;
for(int i=1;i<=n;i++){
int x=l[i],y=r[i];
auto it=st.find(node{x,y});
if(it==st.end()) continue;
auto [L,R]=*it;
//区间不包含或者选了i之后剩余的区间不够选足k个
if(L<=x&&y<=R){
if(cur-calc(L,R)+calc(L,x)+calc(y,R)>=k-ans.size()-1){
cur=cur-calc(L,R)+calc(L,x)+calc(y,R);
ans.push_back(i);
st.erase(it);
st.insert(node{L,x});
st.insert(node{y,R});
}
}
if(ans.size()==k){
for(auto v:ans) cout<<v<<"\n";
return;
}
}
cout<<"-1";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}