分治
这是一些可以让你身败名裂的东西。
点分治
有人已经发现了这个恐怖的事实,事实就是我现在看半年前的点分治代码根本看不懂。
好吧,事实上今天上午也没讲点分治。
cdq分治
这个东西吧,我觉得吧,建议别学,过于困难。其实寒假学过,但是没有一个字是自己写的。
前置知识之求逆序对个数
给定 \(n\) 个数,每个数都有一个权值 \(a_i\),求满足 \(i\le j\) 同时 \(a_i\geq a_j\) 的点对个数。
显然,我们只需按顺序遍历一遍,用树状数组或者线段树维护一下在当前点之前,大于等于当前 \(a_i\) 的点个数即可。
至于它为什么对吧,显然你按顺序遍历就一定满足 \(i\neq j\),同时你维护的就是 \(a_j \geq a_i\),所以正确性十分显然啊。
三维偏序
给定 \(n\) 个数,每个数有3个值 \(u_i,v_i,w_i\),求满足 \(u_i\le u_j,v_i\le v_j,w_i\le w_j\)的点对个数。
假如令 \(w_i=1\),那么答案就很显然了,我们只需要把它当成逆序对来算就可以。
同时,我们灵光一现,发现还有一个东西叫做归并排序,同时,归并排序前对 \(u\) 排序,就一定可以保证左半边的 \(u_i\) 值是小于等于右半边的,同时,归并排序本身就可以维护使 \(v\) 有序,因此,我们只需要再用树状数组维护一下 \(w\) 的顺序即可。
所以 cdq分治的思路就很显然了,分三层维护其有序性,首先在外面对 \(u\) 排个序,然后再使用归并排序,通过合并维护 \(v\) 的顺序,最后在归并排序过程中,我们考虑用个什么东西维护最后一维就好了。
#include<bits/stdc++.h>
#define int long long
#define lowbit(x) (x&(-x))
using namespace std;
int n,k,ans=0;
const int N=2e5+100;
int c[N],sum[N];
struct node {
int x,y,z,ans,cnt;
friend bool operator <(node a,node b) {
if(a.x!=b.x) return a.x<b.x;
if(a.y!=b.y) return a.y<b.y;
return a.z<b.z;
}
}a[N],b[N];
map<node,int>mp;
int read(){int x;cin>>x;return x;}
void insert(int x,int d){
if(x==0) return ;
while(x<=k){
c[x]+=d;
x+=lowbit(x);
}
}
int ask(int x){
int t=0;
while(x){
t+=c[x];
// if(c[x]<0) cerr<<"GGGGG "<<x<<endl;
x-=lowbit(x);
}
return t;
}
void merge(int l,int r) {
if(l==r) return ;
int mid=(l+r)>>1;
merge(l,mid);merge(mid+1,r);
int i=l,j=mid+1,now=l;
for(;i<=mid&&j<=r;now++) {
if(a[i].y<=a[j].y) {
b[now]=a[i];
insert(a[i].z,a[i].cnt);
i++;
}
else {
b[now]=a[j];
b[now].ans+=ask(a[j].z);
j++;
}
}
for(;i<=mid;) {b[now]=a[i];insert(a[i].z,a[i].cnt);i++,now++;}
for(;j<=r;j++,now++) {b[now]=a[j];b[now].ans+=ask(a[j].z);
}
for(int i=l;i<=mid;i++) {insert(a[i].z,-a[i].cnt);}
for(int i=l;i<=r;i++) a[i]=b[i];
return ;
}
signed main() {
freopen("flower.in","r",stdin);
freopen("flower.out","w",stdout);
memset(c,0,sizeof(c));
n=read();k=read();
int x,y,z;
for(int i=1;i<=n;i++) {
x=read();y=read();z=read();
mp[{x,y,z,0,0}]++;
}
int cnt=0;
for(auto y:mp) {
cnt++;
node t=y.first;
t.cnt=y.second;
a[cnt]=t;
a[cnt].ans=0;
}
sort(a+1,a+1+cnt);
merge(1,cnt);
for(int i=1;i<=cnt;i++) {
a[i].ans=a[i].ans+a[i].cnt-1ll;
sum[a[i].ans]+=a[i].cnt;
}
for(int i=0;i<n;i++) {
cout<<sum[i]<<"\n";
}
return 0;
}
导弹拦截
对于第一问,其实就是在问对于 \(i\le j\) 满足 \(a_i\le a_j,b_i\le b_j,c_i\le c_j\) 的最长子序列,我会cdq,每次我们先处理出来左区间的,然后从左区间往右区间转移,转移完了之后,我们让右区间自己转移转移,本质上的转移顺序和暴力是一样的。那么,你就获得了40pts的高分。
接下来,对于第二问,你考虑怎么办,显然,在转移的同时,维护一个方案数是可行的,这样你就处理出来了一共有多少种最长上升子序列。然后考虑哪些点可以出现在这个最长上升子序列里面,实际上就是你再反着做一遍,设 \(f_i\) 表示以 \(i\) 为结尾的最长子序列,设 \(g_i\) 表示以 \(i\) 为开头的最长上升子序列,那么,当且仅当 \(g_i+f_i-1==maxx\) 时,这个点是会出现在最长上升子序列里面的。然后我们就可以同时维护一下这两个的方案数,最后一除,然后就做完了。
#include<bits/stdc++.h>
#define int long long
#define lowbit(x) ((x)&(-x))
using namespace std;
int n;
const int N=1e5+100;
double anss[N];
pair<int,double> c[N];
struct node {
int h,v,id;
int f=1,g=1;
double p1=1.0,p2=1.0;
} st[N];
vector<int> o;
int read() {int x;cin>>x;return x;}
bool cmp1(node a,node b) {
if(a.h!=b.h) return a.h>b.h;
return a.id<b.id;
}
bool cmp3(node a,node b) {
if(a.h!=b.h) return a.h<b.h;
return a.id<b.id;
}
bool cmp2(node a,node b) {
return a.id<b.id;
}
void add(int x,pair<int,double> y) {
if(x<=0) return;
while(x<=n+5) {
if(c[x].first<y.first)
c[x]=y;
else if(c[x].first==y.first)
c[x].second+=y.second;
x+=lowbit(x);
}
}
pair<int,double> ask(int x) {
pair<int,double> t={0,0};
while(x>0) {
if(t.first<c[x].first)
t=c[x];
else if(t.first==c[x].first)
t.second+=c[x].second;
x-=lowbit(x);
}
return t;
}
void add1(int x,pair<int,double> y) {
if(x<=0) return;
while(x>0) {
if(c[x].first<y.first)
c[x]=y;
else if(c[x].first==y.first)
c[x].second+=y.second;
x-=lowbit(x);
}
}
pair<int,double> ask1(int x) {
pair<int,double> t={0,0};
while(x<=n+5) {
if(t.first<c[x].first)
t=c[x];
else if(t.first==c[x].first)
t.second+=c[x].second;
x+=lowbit(x);
}
return t;
}
void clear(int x) {
while(x<=n+5) {
c[x]=make_pair(0,0.0);
x+=lowbit(x);
}
}
void clear1(int x) {
while(x>0) {
c[x]=make_pair(0,0.0);
x-=lowbit(x);
}
}
void work(int l,int r) {
if(l==r) return;
int mid=(l+r)>>1;
work(l,mid);
vector<node> left_arr, right_arr;
for(int i=l;i<=mid;i++) left_arr.push_back(st[i]);
for(int i=mid+1;i<=r;i++) right_arr.push_back(st[i]);
sort(left_arr.begin(),left_arr.end(),cmp1);
sort(right_arr.begin(),right_arr.end(),cmp1);
int i=0,j=0;
while(i<left_arr.size()||j<right_arr.size()) {
if(i<left_arr.size()&&(j>=right_arr.size()||left_arr[i].h>=right_arr[j].h)) {
add1(left_arr[i].v,make_pair(left_arr[i].f,left_arr[i].p1));
i++;
} else {
pair<int,double> res=ask1(right_arr[j].v);
if(res.first+1>right_arr[j].f) {
right_arr[j].f=res.first+1;
right_arr[j].p1=res.second;
} else if(res.first+1==right_arr[j].f) {
right_arr[j].p1+=res.second;
}
j++;
}
}
for(int i=0;i<left_arr.size();i++) clear1(left_arr[i].v);
for(int i=mid+1;i<=r;i++) {
for(int j=0;j<right_arr.size();j++) {
if(st[i].id==right_arr[j].id) {
st[i]=right_arr[j];
break;
}
}
}
work(mid+1,r);
}
void cdq(int l,int r) {
if(l==r) return;
int mid=(l+r)>>1;
cdq(l,mid);
vector<node> left_arr, right_arr;
for(int i=l;i<=mid;i++) left_arr.push_back(st[i]);
for(int i=mid+1;i<=r;i++) right_arr.push_back(st[i]);
sort(left_arr.begin(),left_arr.end(),cmp3);
sort(right_arr.begin(),right_arr.end(),cmp3);
int i=0,j=0;
while(i<left_arr.size()||j<right_arr.size()) {
if(i<left_arr.size()&&(j>=right_arr.size()||left_arr[i].h<=right_arr[j].h)) {
add(left_arr[i].v,make_pair(left_arr[i].g,left_arr[i].p2));
i++;
} else {
pair<int,double> res=ask(right_arr[j].v);
if(res.first+1>right_arr[j].g) {
right_arr[j].g=res.first+1;
right_arr[j].p2=res.second;
}
else if(res.first+1==right_arr[j].g)
right_arr[j].p2+=res.second;
j++;
}
}
for(int i=0;i<left_arr.size();i++) clear(left_arr[i].v);
for(int i=mid+1;i<=r;i++) {
for(int j=0;j<right_arr.size();j++) {
if(st[i].id==right_arr[j].id) {
st[i]=right_arr[j];
break;
}
}
}
cdq(mid+1,r);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
n=read();
for(int i=1;i<=n;i++) {
st[i].h=read();
st[i].v=read();
st[i].id=i;
o.push_back(st[i].v);
}
sort(o.begin(),o.end());
o.erase(unique(o.begin(),o.end()),o.end());
for(int i=1;i<=n;i++)
st[i].v=lower_bound(o.begin(),o.end(),st[i].v)-o.begin()+1;
work(1,n);
for(int i=1;i<=n/2;i++) swap(st[i],st[n-i+1]);
for(int i=1;i<=n;i++) {
st[i].g=1;
st[i].p2=1.0;
}
cdq(1,n);
for(int i=1;i<=n/2;i++) swap(st[i],st[n-i+1]);
int ans=0;
double cnt=0;
for(int i=1;i<=n;i++) ans=max(ans,st[i].f);
for(int i=1;i<=n;i++)
if(st[i].f==ans) cnt+=st[i].p1;
cout<<ans<<endl;
for(int i=1;i<=n;i++) {
if(st[i].f+st[i].g-1==ans) anss[st[i].id]=(st[i].p1*st[i].p2)/cnt;
else anss[st[i].id]=0.0;
}
for(int i=1;i<=n;i++) printf("%.5lf ",anss[i]);
return 0;
}
线段树分治
容易注意到,三个月之前我开过这一篇总结,但是那个摘要里面的只有标题至今都还是只有标题。
分治
就是,你考虑,对于一些求什么什么区间的问题,我们就把这个问题递归下去做,然后做一做左区间的,做一做右区间的,然后合并一下,就做完了。
消失之物
容易想到,这题可以退背包来写,但是我们今天学分治,所以我们考虑分治,你考虑你每次往下走的时候,都把另一个区间的点给加进来,然后你走到最下面叶子的时候,一定就只有这个点还没有被加进来,然后直接统计答案就好。
然后考虑时间复杂度,显然,每个点最多会被加入 \(\log n\) 次,所以总时间复杂度 \(O(nmlogn)\) 可以接受。
ARC_070_D No Need
这题和上一题本质上是一样的啊,就是你把方案数转换成可达性,然后,你去分治,这个样子的话,你就不会炸int了,就是显然,这题你不能退背包,因为你会炸。
当然,如果你是一个头铁的人,那么,容易发现,你只需要和写哈希的时候一样,取成双模数就好,一半没人闲得慌卡你。
这里插播一句,曾经啊,有一场abc,我们发现,E是哈希板子,这还不是手到擒来,于是作者和Air就开始大力码hash,由于作者双哈希没封装,被Air吐槽马蜂丑陋,结果交上去发现只有作者能过,不知为何封装就是过不去,然后Air就炸了,最后被迫写的作者马蜂丑陋的那一版。
最大公约数
你考虑,这题有一个性质啊,你左端点固定,右端点越远,是不是gcd就越小啊,于是考虑gcd的突变点,你考虑它每次突变,起码是要/2 的,所以,容易发现,最多会有 \(\log(n)\) 个突变点,所以,你怎么处理出来那个突变点,然后做一做就好了。
不过吧,这一种做法,代码非常之难写,所以我们考虑换一个思路,我们直接分治,然后这个时候你直接暴力扫左右区间的突变点,接着你 \(O(log^2(n)\) 地跑一跑,然后这不就做完了吗。