Contest Link
\(\text{By DaiRuiChen007}\)
*A. [P11984] 占卜 3 (8.5)
Problem Link
首先朴素的想法就是用 \(a\) 个 \(0\) 和 \(b\) 个 \(1\) 表示 \(\binom{a}{a+b}\) 级别的信息。
有几个问题需要解决:首先如何确定要发送的信息,可以想到用最后 \(k\) 个元素发送前 \(n-k\) 个元素中 \(1\) 的个数 \(q\)。
对于最后 \(k\) 个元素,考虑把这些元素全部放进原串中,这样就能传递信息了。
但此时我们无法确定最后 \(k\) 个元素是 \(0\) 还是 \(1\),所以要考虑构造一些策略。
可以想到类似插板的构造,生成 \(p\) 个板,然后把 \(k\) 个元素放进去,只关心两个板之间的元素数而不关心颜色,方案数 \(\binom{k+p}p\)。
那么对不同颜色的分界点可以用连续段处理,具体来说用前 \(n-k\) 个元素构造 \(p\) 个 \(0\) 加上 \(p\) 个 \(1\),然后每个元素在异色的一侧找到所属的位置插入,然后把两侧每段元素数加起来即可。
此时需要 \(k+2p\) 个元素,取 \(k=10,p=4\) 可以得到代价为 \(18\) 的构造。
如果前缀中 \(0,1\) 的个数不足 \(p\),简单特判一下即可。
为了增加能表示的信息量,我们可以使用串长的信息,即对 \(k=0,1,2,\dots,b\) 分别表示某种信息。
具体来说就是给每个 \((p,k)\) 从小到大分配一个要表示的区间 \([L_k,R_k]\),如果 \(q\not\in[L_k,R_k]\),那么 \(k\gets k-1\) 并把 \(q\) 加上 \(a_{n-k}\)。
那么我们要防止 \(q+1\) 回到 \([L_k,R_k]\) 的情况,可以通过让 \([L_k,R_k]\) 之间有大小为 \(1\) 的交实现。
此时取 \(\max k=8,p=4\) 满足,进一步的优化就是利用上每个 \(p\) 的信息,即读入前 \(n-k\) 个元素时根据 \(q\) 的值动态加入分隔符,此时 \(\max k=6\) 即合法。
时间复杂度 \(\mathcal O(n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
int DrawCard(int);
void Anna(int n) {static int C[11][11],L[11][11],R[11][11];for(int i=0;i<=10;++i) for(int j=C[i][0]=1;j<=i;++j) C[i][j]=C[i-1][j]+C[i-1][j-1];for(int i=1;i<=4;++i) {L[i][0]=R[i][0]=R[i-1][10-i+1]+1;for(int j=1;j<=10-i;++j) L[i][j]=R[i][j-1],R[i][j]=L[i][j]+C[i+j][j]-1;}int op=-1,c0=0,c1=0,z=0,i=0,x;for(;c1+n-i>10;++i) {x=DrawCard(op),op=-1;if(x&&(++z>R[c1][10-c1])) ++c1,op=c0;if(!x&&c0<4) ++c0,op=0;}if(c0<4||!c1) {for(;i<n;++i) {x=DrawCard(op),op=-1;if(x==(c0<4)) op=0;}return DrawCard(op),void();}for(;i<n;++i) {x=DrawCard(op),op=-1;if(z>=L[c1][n-i]) break;z+=x;}if(i==n) return DrawCard(op),void();vector <int> w(c1+1,0);for(int p=c1,q=n-i,k=z-L[p][q];~p;--p) {for(;q&&k>=C[p-1+q][q];++w[p],--q) k-=C[p-1+q][q];}for(int l=0,r=c0+c1;i<n;++i) {while(!w.back()) w.pop_back(),++l,--r;if(x) op=l,++l,++r;else op=r;x=DrawCard(op),op=-1,--w.back();}
}
int Bruno(int n,int m,vector<int>S) {static int C[11][11],L[11][11],R[11][11];for(int i=0;i<=10;++i) for(int j=C[i][0]=1;j<=i;++j) C[i][j]=C[i-1][j]+C[i-1][j-1];for(int i=1;i<=4;++i) {L[i][0]=R[i][0]=R[i-1][10-i+1]+1;for(int j=1;j<=10-i;++j) L[i][j]=R[i][j-1],R[i][j]=L[i][j]+C[i+j][j]-1;}vector<int>p0,p1;for(int i=0;i<m;++i) (S[i]?p1:p0).push_back(i);if(p0.size()<4) return n-p0.size()-10+p1.size();if(p1.empty()) return 14-p0.size();int x=min(4,int(p1.end()-lower_bound(p1.begin(),p1.end(),p0[3]))),y=m-4-x,z=L[x][y]+p1.size()-x;for(int l=0,r=S.size()-1;x;++l,--r,--x) {for(;S[l];++l,--y) z+=C[x-1+y][y];for(;!S[r];--r,--y) z+=C[x-1+y][y];}return z;
}
B. [P10068] Line Town (3.5)
Problem Link
设最终序列为 \(a\),观察发现如果最终 \(h_i\to a_j\),则符号变化为 \((-1)^{i-j}\),那么我们知道 \((-1)^i\times \mathrm{sgn}(h_i)=(-1)^j\times \mathrm{sgn}(a_j)\)。
按 \(|h_i|\) 从大到小 dp,每次要把这些元素插进序列两头,要求每个点的 \((-1)^i\times\mathrm{sgn}(h_i)\) 一定是 \(\pm 1\) 交错的。
那么 dp 记录当前序列开头和结尾要求的符号,转移的时候分讨每段长度的奇偶性,预处理按 $+,-,+,-,\dots $ 或 \(-,+,-,+,\dots\) 的顺序填前缀或后缀的代价。
对于一对可能的逆序对 \((a_i,a_j)\),在 \(k=\max(|a_i|,|a_j|)\) 处计算贡献,容易发现此时另一个元素一定在 \(-k,k\) 所填位置中间,所以只要树状数组简单处理逆序对即可。
可以发现 \(h_i=0\) 的点只要忽略即可,不会影响转移的处理。
时间复杂度 \(\mathcal O(n\log n)\)。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5;
int n,m,a[MAXN],b[MAXN],vl[MAXN];
vector <int> p[MAXN][2];
ll f[2][2],g[2][2],L[2][MAXN],R[2][MAXN];
struct BIT1 {int tr[MAXN],s,op;void add(int x,int v) { for(x=(op?n-x+1:x);x<=n;x+=x&-x) tr[x]+=v; }int qry(int x) { for(x=(op?n-x+1:x),s=0;x;x&=x-1) s+=tr[x]; return s; }
} TL,TR,TO;
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n,TR.op=1;for(int i=1;i<=n;++i) {cin>>a[i],b[i]=(a[i]<0)^(i&1);if(a[i]) vl[++m]=abs(a[i]);}sort(vl+1,vl+m+1),m=unique(vl+1,vl+m+1)-vl-1;for(int i=1;i<=n;++i) if(a[i]) {if(a[i]>0) a[i]=lower_bound(vl+1,vl+m+1,a[i])-vl;else a[i]=vl-lower_bound(vl+1,vl+m+1,-a[i]);p[abs(a[i])][b[i]].push_back(i);}for(int i=1;i<=n;++i) TL.add(i,1),TR.add(i,1);memset(f,0x3f,sizeof(f)),f[0][n&1]=0;for(int c=m;c>=1;--c) {int d=(int)p[c][0].size()-(int)p[c][1].size(),s=p[c][0].size()+p[c][1].size();if(abs(d)>2) return cout<<"-1",0;memset(g,0x3f,sizeof(g));for(int o:{0,1}) for(int i:p[c][o]) TR.add(i,-1);for(int o:{0,1}) {int sz=d>0?2*p[c][1].size()+1:2*p[c][0].size();for(int i=0;i<sz;++i) {int x=p[c][i&1][i/2],y=p[c][i&1].rbegin()[i/2];TL.add(x,-1),L[o][i+1]=L[o][i]+TL.qry(x);R[o][i+1]=R[o][i]+TR.qry(y)+TO.qry(y),TO.add(y,1);}for(int i=0;i<sz;++i) TL.add(p[c][i&1][i>>1],1),TO.add(p[c][i&1].rbegin()[i/2],-1);swap(p[c][0],p[c][1]),d*=-1;}if(d==-2) {for(int i=1;i<s;i+=2) {g[0][0]=min(g[0][0],f[1][1]+L[1][i]+R[1][s-i]);}} else if(d==-1) {for(int i=1;i<=s;i+=2) for(int x:{0,1}) {g[x][0]=min(g[x][0],f[x][1]+L[x][s-i]+R[1][i]);g[0][x]=min(g[0][x],f[1][x]+L[1][i]+R[x][s-i]);}} else if(d==0) {for(int i=0;i<=s;i+=2) for(int x:{0,1}) for(int y:{0,1}) {g[x][y]=min(g[x][y],f[x][y]+L[x][i]+R[y][s-i]);}for(int i=1;i<s;i+=2) {g[1][0]=min(g[1][0],f[0][1]+L[0][i]+R[1][s-i]);g[0][1]=min(g[0][1],f[1][0]+L[1][i]+R[0][s-i]);}} else if(d==1) {for(int i=1;i<=s;i+=2) for(int x:{0,1}) {g[x][1]=min(g[x][1],f[x][0]+L[x][s-i]+R[0][i]);g[1][x]=min(g[1][x],f[0][x]+L[0][i]+R[x][s-i]);}} else {for(int i=1;i<s;i+=2) {g[1][1]=min(g[1][1],f[0][0]+L[0][i]+R[0][s-i]);}}memcpy(f,g,sizeof(f));for(int o:{0,1}) for(int i:p[c][o]) TL.add(i,-1);}ll ans=min({f[0][0],f[0][1],f[1][0],f[1][1]});cout<<(ans<1e18?ans:-1)<<"\n";return 0;
}
C. [P10851] 活动面基 (4)
Problem Link
首先如果是树,可以考虑每个点填深度除以二,奇数轮上取整,偶数轮下取整,那么每个点会走到根,然后在停一轮随机向下。
那么为了让每个点不随即向下,可以选一个叶子的父亲当根,然后任何时候都让叶子与根同色,那么每个点到根后不会停下,而是走到叶子上,然后下一轮返回根,并不断重复,很显然这个构造就成功了。
然后考虑一般图,尝试取出一棵满足条件的生成树,首先取 dfs 树时非树边并不会影响构造。
但我们不能保证根有叶子,考虑优化构造,对于这个特殊的儿子,我们依然让其与根保持同色,但其儿子内部深度都减一(可以看成把这两个点同时当成根进行构造),正确性显然。
那么我们找到一个 dfs 树满足存在根节点的某个儿子,使得其所有儿子与根无边,随机化即可。
时间复杂度 \(\mathcal O(n+m)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=105;
mt19937 rnd(time(0));
vector <int> G[MAXN];
int n,m,rt,p,d[MAXN],c[MAXN];
void dfs(int u) {bool ok=d[u]==2;for(int v:G[u]) if(!d[v]) {d[v]=d[u]+1,c[v]=d[u]>1?c[u]:v,dfs(v);if(ok) for(int w:G[v]) ok&=d[w]>1;}if(ok) p=u;
}
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1,u,v;i<=m;++i) cin>>u>>v,G[u+1].push_back(v+1),G[v+1].push_back(u+1);for(int _=0;_<1000;++_) {for(int i=1;i<=n;++i) shuffle(G[i].begin(),G[i].end(),rnd),d[i]=c[i]=0;rt=rnd()%n+1,d[rt]=1,dfs(rt);if(p) break;}cout<<600<<"\n";for(int t=0;t<600;++t) {for(int i=1;i<=n;++i) {if(t&1) cout<<(d[i]+1)/2<<" \n"[i==n];else cout<<d[i]/2+1-(c[i]==p)<<" \n"[i==n];}}return 0;
}
D. [P11993] 迁移计划 (2.5)
Problem Link
刻画某个 \(a_u\) 当前转移到 \(x\),相当于当前转移到深度为 \(d_x\) 的所有点中,初始在 \(x\) 子树内的所有点。
因此我们用线段树合并维护每个深度对应的所有 \(a_u\),然后查询时计算 dfn 在 \(x\) 子树内的权值之和。
时间复杂度 \(\mathcal O((n+q)\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e6+5;
struct Segt {ll tr[MAXN*40];int ls[MAXN*40],rs[MAXN*40],tot;void add(int x,ll z,int l,int r,int &p) {if(!p) p=++tot;tr[p]+=z;if(l==r) return ;int mid=(l+r)>>1;x<=mid?add(x,z,l,mid,ls[p]):add(x,z,mid+1,r,rs[p]);}void merge(int l,int r,int q,int &p) {if(!p||!q) return p|=q,void();tr[p]+=tr[q];if(l==r) return ;int mid=(l+r)>>1;merge(l,mid,ls[q],ls[p]),merge(mid+1,r,rs[q],rs[p]);}ll qry(int ul,int ur,int l,int r,int p) {if(ul<=l&&r<=ur) return tr[p];int mid=(l+r)>>1; ll s=0;if(ul<=mid) s+=qry(ul,ur,l,mid,ls[p]);if(mid<ur) s+=qry(ul,ur,mid+1,r,rs[p]);return s;}
} T;
int n,q,a[MAXN],rt[MAXN],dfn[MAXN],efn[MAXN],d[MAXN],dcnt;
vector <int> G[MAXN];
void dfs(int u) {T.add(dfn[u]=++dcnt,a[u],1,n,rt[d[u]]);for(int v:G[u]) d[v]=d[u]+1,dfs(v);efn[u]=dcnt;
}
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n;for(int i=2,x;i<=n;++i) cin>>x,G[x].push_back(i);for(int i=1;i<=n;++i) cin>>a[i];dfs(1),cin>>q;for(int op,x,y;q--;) {cin>>op>>x;if(op==1) cin>>y,T.merge(1,n,rt[x],rt[y]),rt[x]=0;else if(op==2) cin>>y,T.add(dfn[x],y,1,n,rt[d[x]]);else cout<<T.qry(dfn[x],efn[x],1,n,rt[d[x]])<<"\n";}return 0;
}
E. [P10437] JOI 之旅 (2.5)
Problem Link
暴力 DDP,维护子树内 \(0\to 1\to 2\) 路径,\(0\to 1,2\to 1\) 的祖先链,以及 \(0,2\) 的数量。
每个点上要额外维护一下轻子树之间转移出 \(0\to 1\to 2\) 或 \(0\to 2\) 路径的方案数。
时间复杂度 \(\mathcal O(q\log^2n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5,N=1<<18;
struct Mat {ll f[6][6];Mat() { memset(f,0,sizeof(f)); }inline friend Mat operator *(const Mat &u,const Mat &v) {Mat w;for(int i=0;i<6;++i) for(int k=0;k<6;++k) for(int j=0;j<6;++j) w.f[i][j]+=u.f[i][k]*v.f[k][j];return w;}
} I;
struct zkw {Mat tr[N<<1];void upd(int x,const Mat &v) {for(tr[x+=N]=v,x>>=1;x;x>>=1) tr[x]=tr[x<<1|1]*tr[x<<1];}Mat qry(int l,int r) {Mat sl=I,sr=I;for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {if(~l&1) sl=tr[l^1]*sl;if(r&1) sr=sr*tr[r^1];}return sr*sl;}
} T;
int n,a[MAXN],fa[MAXN],dep[MAXN],siz[MAXN],hson[MAXN],dfn[MAXN],bot[MAXN],top[MAXN],dcnt;
vector <int> G[MAXN];
array<ll,5> su[MAXN]; ll d012[MAXN],d02[MAXN];
Mat qry(int x) { return T.qry(dfn[x],dfn[bot[x]]); }
void upls(int x,int k) {if(x==1) return ;auto o=qry(x);for(int i=0;i<5;++i) su[fa[x]][i]+=k*o.f[5][i];d012[fa[x]]+=k*(o.f[5][1]*o.f[5][2]+o.f[5][0]*o.f[5][3]);d02[fa[x]]+=k*o.f[5][0]*o.f[5][2];
}
void upmt(int x) {array<ll,5> t=su[x]; Mat o=I;t[4]+=t[1]*t[2]+t[0]*t[3]-d012[x];if(a[x]==0) t[4]+=t[3],++t[0];else if(a[x]==1) t[1]+=t[0],t[3]+=t[2],t[4]+=t[0]*t[2]-d02[x],++o.f[0][1],++o.f[2][3];else t[4]+=t[1],++t[2];for(int i=0;i<5;++i) o.f[5][i]+=t[i];for(int i=0;i<4;++i) o.f[i][4]+=t[3^i];T.upd(dfn[x],o);
}
void dfs1(int u,int fz) {fa[u]=fz,dep[u]=dep[fz]+1,siz[u]=1;for(int v:G[u]) if(v^fz) {dfs1(v,u),siz[u]+=siz[v];if(siz[v]>siz[hson[u]]) hson[u]=v;}
}
void dfs2(int u,int rt) {dfn[u]=++dcnt,top[u]=rt;if(hson[u]) dfs2(hson[u],rt),bot[u]=bot[hson[u]];else bot[u]=u;for(int v:G[u]) if(v!=fa[u]&&v!=hson[u]) dfs2(v,v),upls(v,1);upmt(u);
}
void init(int nn,vector<int>F,vector<int>U,vector<int>V,int) {for(int i=0;i<6;++i) I.f[i][i]=1;n=nn;for(int i=1;i<=n;++i) a[i]=F[i-1];for(int i=0;i<n-1;++i) G[U[i]+1].push_back(V[i]+1),G[V[i]+1].push_back(U[i]+1);dfs1(1,0),dfs2(1,1);
}
void change(int x,int y) {++x;for(int u=x;top[u];u=fa[top[u]]) upls(top[u],-1);a[x]=y;for(int u=x;top[u];u=fa[top[u]]) upmt(u),upls(top[u],1);
}
ll num_tours() { return qry(1).f[5][4]; }
*F. [P11990] 大会(9.5)
Problem Link
首先对于每个 \(?\) 连续段,观察其两侧的字符:
- 如果相同,不妨设是 \(A\),那么这个连续段中有 \(B\) 或 \(C\) 时代价 \(+2\),同时有则代价 \(+3\)。
- 如果不同,不妨设是 \(A,B\),那么这个连续段中有 \(C\) 时代价 \(+1\)。
那么思路就是钦定每个连续段包含 \(A,B,C\) 中的哪些字符,然后判断是否存在合法字符串。
判断是否存在字符串可以看成存在字符到位置的完美匹配,用 Hall 定理刻画限制,要求是 \(A\le X\le A+AB+AC+ABC\),以及关于 \((Y,B),(Z,C)\) 对称的限制,其中 $A,AB,AC,\dots $ 表示可以填入的字符集为对应状态的连续段长度和。
首先我们假设每个段当前只能填入两侧的字符,并分讨此时有几个 \(X>A+AB+AC+ABC\)。
-
一个都不存在:
此时只要调整使得 \(A\le X,B\le Y,C\le Z\),那么在 \(A,B,C\) 三种段中分别降序排序后二分,每段代价 \(+2\)。
-
存在一个:
不妨设 \(X>A+AB+AC+ABC\),那么首先选择若干 \(B,C\) 用 \(+2\) 的代价变成 \(AB,AC\) 使得 \(B\le Y,C\le Z\)。
那么此时我们的决策就是 \(B\to AB,C\to AC,BC\to ABC\),前两种代价为 \(2\),最后一种代价为 \(1\)。
我们会按照长度除以代价贪心,可以用二分斜率模拟该过程。
然后我们可能通过一些调整使得代价 \(-1\),此时要么删掉一个代价为 \(1\) 的段,要么把一个代价为 \(2\) 的段换成一个代价为 \(1\) 的段,容易维护之,注意相同斜率的元素处理有一些细节。
-
存在两个:
不妨设 \(X\le A+AB+AC+ABC\),那么首先选择一些 \(A\) 变成 \(AB\) 或 \(AC\) 使得 \(A\le X\),不妨设此时至少要变化 \(k\) 个段,则答案 \(\ge 2k\)。
观察发现由于 \(Y>B+AB+BC+ABC\),因此 \(X+Z\le A+AC+C\),从而 \(Z-(C+AC+BC+ABC)\le A-X\)。
所以我们把这 \(k\) 个段全部变成 \(ABC\) 时必定合法,可以得到 \(3k\) 次操作还原的构造。
但我们注意到 \(Y-(B+AB+BC+ABC)+Z-(C+AC+BC+ABC)=A-BC-ABC-X\le A-X\)。
所以从前往后选择若干个段直到和 \(\ge Y-(B+AB+BC+ABC)\),从后往前选若干个段直到和 \(\ge Z-(C+AC+BC+ABC)\),则这两个前后缀交 \(\le 1\),此时前缀 \(A\to AB\),后缀 \(A\to AC\),交 \(A\to ABC\),即可得到 \(\le 2k+1\) 的解。
所以只要判断答案是否 \(=2k\),很显然此时必须把 \(A\) 的前 \(k\) 个段划分成两部分 \(S_B,S_C\),要求两部分的和都大于某个数,即 \(\mathrm{sum}(S_B)\) 在一个区间中。
对所有 \(A\) 中的段从前往后做背包,对每个 \(\mathrm{sum}(S_B)\) 预处理首次被表出的时刻,然后 ST 表维护区间最小值是否 \(\le k\) 即可判断。
由于 \(A\) 内部段长降序排列因此可以用多重背包优化,或者直接使用
std::bitset优化。 -
存在三个:由于 \(X+Y+Z=A+B+C+AB+AC+BC+ABC\),显然不可能。
时间复杂度 \(\mathcal O\left(\dfrac{n^2}\omega+q\log^2n\right)\),可以精细实现做到 \(\mathcal O(n\sqrt n+q\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5;
int n,q,df=0,st[3][20][MAXN];
int qmn(int c,int l,int r) {if(l>r) return n+1;int k=__lg(r-l+1);return min(st[c][k][l],st[c][k][r-(1<<k)+1]);
}
char a[MAXN];
vector <int> f[3],g[3],sf[3],sg[3];
int lb(vector<int>&o,int z) { return lower_bound(o.begin(),o.end(),z)-o.begin(); }
int rb(vector<int>&o,int z) { return upper_bound(o.begin()+1,o.end(),z,greater<>())-o.begin()-1; }
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>(a+1)>>q;for(int i=2,j=1;i<=n;++i) if(a[i]!='?') {if(i-j>1) {if(a[i]==a[j]) f[a[i]-'A'].push_back(i-j-1);else g[3^(a[i]-'A')^(a[j]-'A')].push_back(i-j-1);}df+=a[i]!=a[j],j=i;}for(int c:{0,1,2}) {sort(f[c].begin(),f[c].end(),greater<>()),f[c].insert(f[c].begin(),0),sf[c]=f[c];sort(g[c].begin(),g[c].end(),greater<>()),g[c].insert(g[c].begin(),0),sg[c]=g[c];for(int i=1;i<(int)sf[c].size();++i) sf[c][i]+=sf[c][i-1];for(int i=1;i<(int)sg[c].size();++i) sg[c][i]+=sg[c][i-1];for(int i=1;i<=n;++i) st[c][0][i]=n+1;static bitset<MAXN> pr,nx;pr.reset(),pr[0]=1;for(int i=1;i<(int)f[c].size();++i) {nx=pr,nx<<=f[c][i],nx|=pr,nx^=pr,pr^=nx;for(int k=nx._Find_first();k<=n;k=nx._Find_next(k)) st[c][0][k]=i;}for(int k=1;k<20;++k) for(int i=0;i+(1<<k)-1<=n;++i) {st[c][k][i]=min(st[c][k-1][i],st[c][k-1][i+(1<<(k-1))]);}}for(int b[3],w[3];q--;) {cin>>b[0]>>b[1]>>b[2];int ct=0,p=0;for(int c:{0,1,2}) {w[c]=b[c]-sf[c].back()-sg[(c+1)%3].back()-sg[(c+2)%3].back();if(w[c]>0) ++ct,p^=c;else w[c]=lb(sf[c],sf[c].back()-b[c]);}if(ct==0) { cout<<df+2*(w[0]+w[1]+w[2])<<"\n"; continue; }if(ct==2) { p^=3,cout<<df+2*w[p]+(qmn(p,w[(p+1)%3],sf[p][w[p]]-w[(p+2)%3])>w[p])<<"\n"; continue; }int x=(p+1)%3,y=(p+2)%3,z=0; //q(f[x] f[y] g[p])>=w[p]auto chk=[&](int k) {return sf[x][max(w[x],rb(f[x],k))]+sf[y][max(w[y],rb(f[y],k))]+sg[p][rb(g[p],(k+1)/2)]-w[p];};for(int k=1<<__lg(n);k;k>>=1) if(k+z<=n&&chk(k+z)>=0) z+=k;int h=chk(z),u=max(w[x],rb(f[x],z)),v=max(w[y],rb(f[y],z)),t=rb(g[p],(z+1)/2),s=2*u+2*v+t;s-=h/z*2,h%=z;if(t&&h>=g[p][t]) --s;else if(t+1<(int)g[p].size()) s-=(u>w[x]&&h>=f[x][u]-g[p][t+1])||(v>w[y]&&h>=f[y][v]-g[p][t+1]);cout<<df+s<<"\n";}return 0;
}
*G. [P11983] 展览会 3 (8.5)
Problem Link
首先可以想到按值从大到小贪心地确定每个区间。
那么我们的思路就是从前往后,每个没被确定的区间填成当前的最大值 \(v\),要求加入后可以通过不超过 \(c\) 个点覆盖所有区间,\(c\) 是 \(v\) 的出现次数。
考虑如何快速维护判定,首先我们会加入序列的一个前缀,且停止加入时的线段覆盖集大小恰好为 \(c\)。
首先如何计算这个前缀 \(p\),注意到 \(\sum p\le m\),因此只要 \(\mathcal O(p)\) 复杂度确定即可。
那么可以倍增,先求最小的 \(2^k>p\),然后再依次加入 \(2^{k-1}\sim 2^0\),可以满足每次检验的长度之和为 \(\mathcal O(p\log p)\)。
然后对于一条新的线段,可以用调整法证明其可以加入当且仅当存在一个点可以放入的范围与该线段有交,而该每个点的范围可以从前往后选线段左端点以及从后往前选线段右端点计算得到。
那么现在我们有 \(c\) 个区间 \([L_i,R_i]\) 表示每个点可以放入的范围,对于一条新的线段 \([l,r]\),如果存在 \([L_i,R_i]\subseteq [l,r]\),那么很显然 \([l,r]\) 线段合法。
否则 \([l,r]\) 与 \(\le 2\) 个区间有交,如果只有一个,那么直接把 \([L_i,R_i]\) 与 \([l,r]\) 取交,否则 \(l\in[L_i,R_i],r\in [L_{i+1},R_{i+1}]\),那么 \(L_i>l\) 时 \(R_{i+1}\gets r\),反之亦然。
那么我们用堆维护第二种限制,每次更新 \([L_i,R_i]\) 的时候弹出堆顶被满足的限制,然后递归 \([L_{i-1},R_{i-1}],[L_{i+1},R_{i+1}]\) 上的限制即可。
这里注意前 \(p\) 条线段中也可能存在对 \([L_i,R_i]\) 的限制,因此也要加入。
然后我们要用数据结构快速找到最小的线段,做法是先用数据结构维护当前所有的 \([l,r]\),然后每次询问与一个 \([L_i,R_i]\) 有交线段中标号最小的一条,加入堆中并从数据结构中删除,用堆按标号顺序依次处理 \([l,r]\),弹出 \([l,r]\) 时把对应的 \([L_i,R_i]\) 有交线段的新最小值加入堆。
注意到此时堆顶某个 \([l,r]\) 没有加入成功必定对应一次对 \([L_i,R_i]\) 的更新,因此考虑到线段与被插入的线段同级。
那么数据结构要支持加删线段,快速维护有交线段最小值,有交可以刻画成两个线段在线段树上的叶子节点存在祖先后代关系,那么线段树每个节点上用可删堆动态维护最小值,以及子树内的最小值即可。
时间复杂度 \(\mathcal O((n+m)\log^2 n)\),精细实现可以做到 \(\mathcal O((n+m)\log n)\)。
具体来说第一部分可以用归并快速求出 \(2^k\),然后每次对前 \(2^k\) 个元素排序后的结果处理即可优化到单 \(\log\)。
第二部分讨论 \([l,r],[L_i,R_i]\) 交情况,如果 \(l\) 或 \(r\) 属于 \([L_i,R_i]\) 平凡,只要考虑最小点 \(l\le L_i\le R_i\le r\) 的情况。
如果发现某个时刻 \([l,r]\) 满足该限制,那么 \([l,r]\) 无论如何必定能被加入,因此只要在更新某个 \([L_i,R_i]\) 时弹出所有包含 \([L_i,R_i]\) 的 \([l,r]\) 即可。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5,inf=1e9;
int n,m,a[MAXN],bl[MAXN],br[MAXN],cl[MAXN],cr[MAXN],ans[MAXN];
typedef array<int,2> pii;
template<typename T>using PQ=priority_queue<T,vector<T>,greater<>>;
struct Heap {PQ<int> qi,qo;void ins(int v,int o) { (o>0?qi:qo).push(v); }int top() {while(qo.size()&&qi.top()==qo.top()) qi.pop(),qo.pop();return qi.top();}
};
int dsu[MAXN];
int find(int x) { return x^dsu[x]?dsu[x]=find(dsu[x]):x; }
struct Segt {Heap h[1<<18|5];int mn[1<<18|5];void init(int l=1,int r=n,int p=1) {mn[p]=inf,h[p].ins(inf,1);if(l==r) return ;int mid=(l+r)>>1;init(l,mid,p<<1),init(mid+1,r,p<<1|1);}void psu(int p) { mn[p]=min({mn[p<<1],mn[p<<1|1],h[p].top()}); }void add(int x,int o,int ul,int ur,int l=1,int r=n,int p=1) {if(ul<=l&&r<=ur) return h[p].ins(x,o),(l<r?psu(p):(mn[p]=h[p].top(),void()));int mid=(l+r)>>1;if(ul<=mid) add(x,o,ul,ur,l,mid,p<<1);if(mid<ur) add(x,o,ul,ur,mid+1,r,p<<1|1);psu(p);}int qry(int ul,int ur,int l=1,int r=n,int p=1) {if(ul<=l&&r<=ur) return mn[p];int mid=(l+r)>>1,s=h[p].top();if(ul<=mid) s=min(s,qry(ul,ur,l,mid,p<<1));if(mid<ur) s=min(s,qry(ul,ur,mid+1,r,p<<1|1));return s;}
} T;
int id[MAXN];
int chk(int k) {for(int i=1;i<=k;++i) id[i]=find(id[i-1]+1);sort(id+1,id+k+1,[&](int x,int y){ return br[x]<br[y]; });int ct=0,z=0;for(int i=1;i<=k;++i) if(bl[id[i]]>z) z=br[id[i]],++ct;return ct;
}
PQ<pii> fl[MAXN],fr[MAXN],Q;
void dfs(int u) {int x=cr[u+1],y=cl[u-1];for(;fl[u].size()&&-fl[u].top()[0]>cr[u];fl[u].pop()) cr[u+1]=min(cr[u+1],fl[u].top()[1]);for(;fr[u].size()&&fr[u].top()[0]<cl[u];fr[u].pop()) cl[u-1]=max(cl[u-1],fr[u].top()[1]);if(cr[u+1]<x) dfs(u+1);if(cl[u-1]>y) dfs(u-1);
}
void push(int x) {int y=T.qry(cl[x],cr[x]);if(y<inf) T.add(y,-1,bl[y],br[y]),Q.push({y,x});
}
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>m,T.init(),iota(dsu+1,dsu+m+2,1);for(int i=1,x;i<=n;++i) cin>>x,++a[x];for(int i=1;i<=m;++i) cin>>bl[i]>>br[i],T.add(i,1,bl[i],br[i]);for(int w=n,ct=m;w>=1&&ct;--w) if(a[w]) {int k=1;for(;2*k<=ct&&chk(2*k)<=a[w];) k<<=1;for(int i=k>>1;i;i>>=1) if(k+i<=ct&&chk(k+i)<=a[w]) k+=i;if(!k) continue;int q=chk(k);auto lim=[&](int x) {int u=lower_bound(cr+1,cr+q+1,bl[x])-cr,v=upper_bound(cl+1,cl+q+1,br[x])-cl-1;if(u>v) return T.add(x,1,bl[x],br[x]);dsu[x]=x+1,ans[x]=w,--ct;if(u==v) cl[u]=max(cl[u],bl[x]),cr[u]=min(cr[u],br[x]),dfs(u);else if(v==u+1) fl[u].push({-bl[x],br[x]}),fr[v].push({br[x],bl[x]});};for(int i=1,z=0,j=0;i<=k;++i) if(bl[id[i]]>z) cr[++j]=z=br[id[i]];sort(id+1,id+k+1,[&](int x,int y){ return bl[x]>bl[y]; });for(int i=1,z=n+1,j=q;i<=k;++i) if(br[id[i]]<z) cl[j--]=z=bl[id[i]];for(int i=1;i<=k;++i) T.add(id[i],-1,bl[id[i]],br[id[i]]),lim(id[i]);for(int i=1;i<=q;++i) push(i);for(int las=0,x,c;Q.size();las=x) {x=Q.top()[0],c=Q.top()[1],Q.pop(),push(c);if(x>las) lim(x);}for(int i=1;i<=q;++i) PQ<pii>().swap(fl[i]),PQ<pii>().swap(fr[i]);}for(int i=1;i<=m;++i) cout<<ans[i]<<"\n";return 0;
}
H. [P11986] 救护车 (3)
Problem Link
首先只考虑匹配左上角和右下角的点,容易发现分别是 \(x+y\) 的一段前缀和后缀,否则交换后必定更优。
类似地左下角和右上角的点按 \(x-y\) 排序即可,那么直接暴力枚举对应的两个分界线,此时点集分成四部分,每部分的点只有两种决策。
那么可以直接记录其中一侧的代价,对另一侧背包,然后依次 \(\mathcal O(L)\) 合并四个区域的贡献。
先枚举 \(x+y\) 分界线,然后按 \(x-y\) 排序后求出前缀后缀背包即可快速得到四个区域的贡献。
时间复杂度 \(\mathcal O(n^2L)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+5;
struct poi { int u,v; } a[165];
int n,m,q,f[MAXN],g[165][MAXN],X[MAXN],Y[MAXN];
void solve(vector<poi>b,int *z) {sort(b.begin(),b.end(),[&](poi x,poi y){ return x.v<y.v; });int sz=b.size();memset(f,0,(m+1)<<2),memset(g[sz],0,(m+1)<<2),memset(z,0x3f,(m+1)<<2);for(int i=sz-1;~i;--i) {int *nw=g[i],*pr=g[i+1];for(int j=0;j<b[i].u&&j<=m;++j) nw[j]=pr[j]+q-b[i].v;for(int j=b[i].u;j<=m;++j) nw[j]=min(pr[j-b[i].u],pr[j]+q-b[i].v);}z[0]=g[0][m];for(int i=0;i<sz;++i) {for(int j=m;j>=b[i].u;--j) f[j]=min(f[j-b[i].u],f[j]+q+b[i].v);for(int j=0;j<b[i].u&&j<=m;++j) f[j]+=q+b[i].v;int *w=g[i+1];for(int j=0;j<=m;++j) if(f[j]<=m) z[f[j]]=min(z[f[j]],w[m-j]);}for(int i=1;i<=m;++i) z[i]=min(z[i],z[i-1]);
}
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>q>>n>>m,--q,m/=2;for(int i=1,x,y;i<=n;++i) cin>>x>>y,a[i]={x+y-2,x-y};sort(a+1,a+n+1,[&](poi i,poi j){ return i.u<j.u; });for(int i=0;i<=n;++i) {vector <poi> L,R;for(int j=1;j<=i;++j) L.push_back(a[j]);for(int j=i+1;j<=n;++j) R.push_back({2*q-a[j].u,a[j].v});solve(L,X),solve(R,Y);for(int j=0;j<=m;++j) if(X[j]+Y[m-j]<=m) return cout<<"Yes\n",0;}cout<<"No\n";return 0;
}
*I. [P10436] 卡牌收集 (8)
Problem Link
分讨 \((A,B)\) 如何生成:如果是 \((A,B)+(\ge A,\ge B)/(\le A,\le B)\),那么可以不断拆分直到不存在 \((A,B)\)。
如果答案就是从某个原始的 \((A,B)\) 中生成,那么要求就是其前缀和后缀能生成 \((\ge A,\ge B)\) 或 \((\le A,\le B)\),维护 \(A_{\max},A_{\min},B_{\max},B_{\min}\) 即可。
否则分讨一下不由 \((A,B)\) 生成的情况,即 \((A,\ge B)+(\ge A,B)\)、\((A,\le B)+(\le A,B)\),然后我们可能在拆分过程中在左右两侧生成了若干 \((\ge A,\ge B)\) 或 \((\le A,\le B)\),很显然这些元素可以直接与这两个元素合并。
因此只要判断前缀能否生成 \((A,\le B)\),剩余的情况都可以通过翻转序列和值域处理。
继续分讨 \((A,\le B)\) 可以由 \((A,*)+(\ge A,\le B)\) 或 \((A,\le B)+(\le A,\le B)\) 生成。
对于情况二,类似地,不妨假设在某个时刻 \((A,\ge B)\) 变成了 \((A,*)+(\ge A,\le B)\),那么依旧可以忽略两侧 \((\le A,\le B)\) 的影响。
所以只要考虑情况一,首先生成 \((A,*)\) 只要区间内部存在 \((A,*)\) 即可,而 \((\ge A,\le B)\) 需要 \((\ge A,\le B)+(\ge A,*)\) 或 \((\ge A,\le B)+(\le B,*)\) 生成。
那么不断拆分,要求就是存在一个 \((\ge A,\le B)\) 前后缀均存在 \((\ge A,*)\) 或 \((\le B,*)\),并且存在 \((A,*)\)。
可以验证由 \((A,\ge B)\) 合并若干 \((\le A,\le B)\) 的序列也会被该条件正确判定。
那么只要对每个询问求出最小合法前缀即可。
可以看成简单二维偏序问题,扫描线解决。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef basic_string<int> vi;
const int MAXN=2e5+5,N=1<<18;
int n,m,a[MAXN],b[MAXN],c[MAXN],d[MAXN];
bool ans[MAXN],ok[MAXN];
int f[MAXN],g[MAXN];
vi P[MAXN],Q[MAXN];
struct zkw {int tr[N<<1];void init() { memset(tr,0x3f,sizeof(tr)); }void upd(int x,int v) { for(x+=N;x;x>>=1) tr[x]=min(tr[x],v); }int qry(int x,int v) {for(x+=N;x;x>>=1) if((~x&1)&&tr[x^1]<=v) break;if(!x) return n+1;for(x^=1;x<N;) x=x<<1|(tr[x<<1]>v);return x-N;}
} T;
int mn[MAXN];
void sol(vi &qi) {for(int i=1;i<=n;++i) P[i].clear(),Q[i].clear();for(int i=1;i<=n;++i) P[a[i]]+=i;for(int i:qi) Q[c[i]]+=i;T.init(),mn[0]=n+1;for(int i=1;i<=n;++i) mn[i]=min(mn[i-1],b[i]);for(int i=n,p=n+1;i;--i) {for(int x:P[i]) T.upd(x,b[x]),p=min(p,x);for(int y:Q[i]) {f[y]=T.qry(0,d[y]);if(f[y]==p&&p>1&&mn[p-1]>d[y]) f[y]=T.qry(f[y],d[y]);f[y]=max(f[y],P[i][0]);}}
}
void solve(vi &qi) {sol(qi),swap(f,g);reverse(a+1,a+n+1),reverse(b+1,b+n+1),swap(a,b),swap(c,d);sol(qi);reverse(a+1,a+n+1),reverse(b+1,b+n+1),swap(a,b),swap(c,d);for(int i:qi) ans[i]|=f[i]+g[i]<=n;
}
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=1;i<=n;++i) cin>>a[i]>>b[i];vi va(a+1,a+n+1),vb(b+1,b+n+1);sort(va.begin(),va.end()),va.erase(unique(va.begin(),va.end()),va.end()),va.push_back(2e9);sort(vb.begin(),vb.end()),vb.erase(unique(vb.begin(),vb.end()),vb.end()),vb.push_back(2e9);for(int i=1;i<=n;++i) {a[i]=lower_bound(va.begin(),va.end(),a[i])-va.begin()+1;b[i]=lower_bound(vb.begin(),vb.end(),b[i])-vb.begin()+1;}vi qi,qx;for(int i=1;i<=m;++i) {cin>>c[i]>>d[i];int x=lower_bound(va.begin(),va.end(),c[i])-va.begin();int y=lower_bound(vb.begin(),vb.end(),d[i])-vb.begin();if(va[x]==c[i]&&vb[y]==d[i]) c[i]=x+1,d[i]=y+1,qi+=i;}for(int i=1,al=a[1],ar=a[1],bl=b[1],br=b[1];i<=n;++i) {ok[i]=(al<=a[i]&&bl<=b[i])||(ar>=a[i]&&br>=b[i]);al=min(al,a[i]),ar=max(ar,a[i]),bl=min(bl,b[i]),br=max(br,b[i]);}map <pair<int,int>,int> okp;for(int i=n,al=a[n],ar=a[n],bl=b[n],br=b[n];i>=1;--i) {ok[i]&=(al<=a[i]&&bl<=b[i])||(ar>=a[i]&&br>=b[i]);al=min(al,a[i]),ar=max(ar,a[i]),bl=min(bl,b[i]),br=max(br,b[i]);if(ok[i]) okp[{a[i],b[i]}]=i;}for(int i:qi) {if(okp.count({c[i],d[i]})) ans[i]=true;else qx+=i;}qi.swap(qx),qx.clear(),solve(qi);for(int i=1;i<=n;++i) a[i]=n-a[i]+1,b[i]=n-b[i]+1;for(int i:qi) if(!ans[i]) qx+=i,c[i]=n-c[i]+1,d[i]=n-d[i]+1;qi.swap(qx),qx.clear(),solve(qi);reverse(a+1,a+n+1),reverse(b+1,b+n+1);for(int i:qi) if(!ans[i]) qx+=i;qi.swap(qx),qx.clear(),solve(qi);for(int i=1;i<=n;++i) a[i]=n-a[i]+1,b[i]=n-b[i]+1;for(int i:qi) if(!ans[i]) qx+=i,c[i]=n-c[i]+1,d[i]=n-d[i]+1;qi.swap(qx),qx.clear(),solve(qi);for(int i=1;i<=m;++i) if(ans[i]) cout<<i<<" "; cout<<"\n";return 0;
}
J. [P11985] 比太郎之旅 2 (1)
Problem Link
每个点会跳到 Kruskal 重构树上首个 \(\le w_u+L\) 的点的子树内,那么倍增预处理这个祖先,查询时倍增首个包含终点的祖先即可。
时间复杂度 \(\mathcal O((n+q)\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5;
int n,m,w,q,a[MAXN],dsu[MAXN],up[MAXN][20],fa[MAXN][20],dfn[MAXN],efn[MAXN],dcnt;
vector <int> G[MAXN],E[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void dfs(int u) { dfn[u]=++dcnt; for(int v:G[u]) dfs(v); efn[u]=dcnt; }
bool in(int x,int y) { return dfn[y]<=dfn[x]&&dfn[x]<=efn[y]; }
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>m>>w;for(int i=1,x;i<=n;++i) for(int j=1;j<=m;++j) {x=(i-1)*m+j;if(i>1) E[x].push_back(x-m);if(i<n) E[x].push_back(x+m);if(j>1) E[x].push_back(x-1);if(j<m) E[x].push_back(x+1);}vector <int> id; n*=m;for(int i=1;i<=n;++i) cin>>a[i],id.push_back(i);sort(id.begin(),id.end(),[&](int x,int y){ return a[x]<a[y]; });for(int x:id) {dsu[x]=x;for(int y:E[x]) if(dsu[y]&&find(y)!=x) G[x].push_back(y=find(y)),up[y][0]=dsu[y]=x;}for(int i=1;i<=n;++i) if(dsu[i]==i) up[i][0]=i,dfs(i);for(int k=1;k<20;++k) for(int i=1;i<=n;++i) up[i][k]=up[up[i][k-1]][k-1];for(int i=1;i<=n;++i) {int &u=fa[i][0]=i;for(int k=19;~k;--k) if(a[up[u][k]]<=a[i]+w) u=up[u][k];}for(int k=1;k<20;++k) for(int i=1;i<=n;++i) fa[i][k]=fa[fa[i][k-1]][k-1];cin>>q;for(int sx,sy,ex,ey,u,v;q--;) {cin>>sx>>sy>>ex>>ey,u=(sx-1)*m+sy,v=(ex-1)*m+ey;if(!in(v,fa[u][19])) { cout<<"-1\n"; continue; }int x=0;for(int k=19;~k;--k) if(!in(v,fa[u][k])) u=fa[u][k],x+=1<<k;cout<<x+1<<"\n";}return 0;
}
K. [P11987] 集邮比赛 4 (3)
Problem Link
对于一个确定的循环移位,设每种颜色 \(c\) 的出现位置为 \(l_c,r_c\),其中 \(l_c<r_c\),那么答案就是 \(\sum_{x,y}[r_x<l_y]\)。
证明考虑把 \(l_c\) 看成 \(0\),\(r_c\) 看成 \(1\),序列合法当且仅当对应括号串为 \(00\dots 0+11\dots 1\),所以答案为逆序对数。
每对每对颜色 \((x,y)\),分讨其相交关系能算出有贡献的循环移位是若干个区间,考虑对每个位置差分值的影响,那么只要枚举 \(x\),然后贡献相当于二维数点计算 \(y\) 的个数。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5;
const ll inf=4e18;
struct seg { int l,r; } b[MAXN];
int n,q,a[MAXN],cl[MAXN],cr[MAXN];
array<ll,2> f[MAXN];
ll C,vl[MAXN],vr[MAXN];
struct BIT {int tr[MAXN],s;void init() { memset(tr,0,sizeof(tr)); }void add(int x) { for(;x<=2*n;x+=x&-x) ++tr[x]; }int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
} T;
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>C;for(int i=1;i<=2*n;++i) cin>>a[i],(b[a[i]].l?b[a[i]].r:b[a[i]].l)=i;for(int i=1;i<=2*n;++i) cin>>f[i][1];for(int i=1;i<=n;++i) ++cl[b[i].l],++cr[b[i].r];for(int i=1;i<=2*n;++i) cr[i]+=cr[i-1];for(int i=2*n;i>=1;--i) cl[i]+=cl[i+1];for(int i=1;i<=n;++i) {f[b[i].l][0]+=cl[b[i].r+1];f[b[i].r][0]-=cl[b[i].r+1];f[b[i].l][0]+=cr[b[i].l-1];f[b[i].r][0]-=cr[b[i].l-1];f[2*n][0]+=cr[b[i].l-1];}sort(b+1,b+n+1,[&](auto x,auto y){ return x.l>y.l; });for(int i=1;i<=n;++i) {int w=T.qry(b[i].r);f[b[i].l][0]-=w,f[b[i].r][0]+=w,T.add(b[i].r);}sort(b+1,b+n+1,[&](auto x,auto y){ return x.r>y.r; }),T.init();for(int i=1;i<=n;++i) {int w=T.qry(b[i].l);f[b[i].r][0]-=w,f[b[i].l][0]+=w,T.add(b[i].l);}for(int i=2*n;i;--i) f[i][0]+=f[i+1][0];sort(f+1,f+2*n+1),vl[0]=vr[2*n+1]=inf;for(int i=1;i<=2*n;++i) vl[i]=min(vl[i-1],f[i][1]);for(int i=2*n;i;--i) vr[i]=min(vr[i+1],f[i][1]+C*f[i][0]);cin>>q;for(ll k;q--;) {cin>>k,k=1ll*n*n-k;int i=lower_bound(f+1,f+2*n+1,array<ll,2>{k,0})-f;cout<<min(vl[i-1],vr[i]-C*k)<<"\n";}return 0;
}
L. [P9867] kon (3.5)
Problem Link
建出操作树,观察发现 \(u,v\) 有边当且仅当 \(u\to v\) 路径中只有 dfn 最小的边为 W 类型。
设 \(\mathrm{dfn}(u)<\mathrm{dfn}(v)\),如果 \(v\) 在 \(u\) 子树内平凡,否则找到 \(u\to rt\) 首个 W 边的父亲 \(x\),那么 \(v\) 是 \(x\) 子树中点,且满足 \(\mathrm{dfn}(v)\ge \mathrm{dfn}(u)\) 以及 \(v\to rt,x\to rt\) 路径中 W 边数量相同。
那么把所有点按到根路径 W 边数量为第一关键字,dfn 为第二关键字排序,合法的 \(v\) 依旧是一段区间,容易用 BIT 维护。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
struct Edge { int v,w; };
vector <Edge> G[MAXN];
int n=2,m,up[MAXN],a[MAXN],dcnt=0,id[MAXN],rk[MAXN],ct[MAXN],efn[MAXN];
array<int,2>f[MAXN],g[MAXN];
void dfs(int u) {f[u][1]=++dcnt;for(auto e:G[u]) up[e.v]=e.w?u:up[u],f[e.v][0]=f[u][0]+e.w,dfs(e.v);efn[u]=dcnt;
}
struct BIT {int tr[MAXN],s;void add(int x,int z) { for(;x<=n;x+=x&-x) tr[x]+=z; }int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
} T1,T2;
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>m,m+=2;G[1].push_back({2,1}),a[1]=1,a[2]=2;for(int i=3,x;i<=m;++i) {char o; cin>>o>>x;if(o=='?') a[i]=-x;else a[i]=++n,G[x].push_back({n,o=='W'});}dfs(1),iota(id+1,id+n+1,1),sort(id+1,id+n+1,[&](int x,int y){ return f[x]<f[y]; });for(int i=1;i<=n;++i) rk[id[i]]=i,g[i]=f[id[i]];for(int i=1;i<=m;++i) {int x=abs(a[i]),z=0;if(up[x]) {int l=lower_bound(g+1,g+n+1,array<int,2>{f[up[x]][0],f[x][1]})-g;int r=upper_bound(g+1,g+n+1,array<int,2>{f[up[x]][0],efn[up[x]]})-g-1;if(a[i]<0) z+=1+T1.qry(r)-T1.qry(l-1);else ++ct[up[x]],T2.add(l,1),T2.add(r+1,-1);}if(a[i]<0) cout<<z+ct[x]+T2.qry(rk[x])<<"\n";else T1.add(rk[x],1);}return 0;
}
M. [P11988] 宇宙怪盗(4)
Problem Link
首先树上问题很简单,考虑对边分治,然后正反两次询问判断是否跨过这条边,找到一条合法边后在两侧 dfn 序上二分即可。
边分治无法通过,考虑经典的三度化技巧,即将重心 \(u\) 的子树分成两部分,得到两个过 \(u\) 的集合,依然可以类似判断起终点是否分属不同集合,此时边分树深度 \(\log_{1.5}n\)。
很显然可以把同层的边分治用一次询问处理,此时 dfn 序二分的过程无法继续简单执行,因为我们不知道起点终点在哪个连通块中。
可以考虑定向后的图形成的 DAG,如果该 DAG 有解,则将两端点拓扑序不全属于 \([l,r]\) 的边都翻转,就能判断起点终点是否同时在 \([l,r]\) 中,用该方法判定即可。
询问次数 \(2(\log_{1.5}n+\log_2 n)\)。
在确定第一个点所属集合后可以优化第二个点的二分范围,可以做到 \(2\log_{1.5}n+\log n\)。
显然非树边只要按树边的拓扑序定向就没有任何影响,实现时对连通块之间的公共点有一些细节。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#include "thief.h"
using namespace std;
typedef basic_string<int> vi;
const int MAXN=1.5e5+5;
struct Edge { int v,i; };
vector <Edge> G[MAXN];
bool vis[MAXN],ok[MAXN],inq[MAXN];
int siz[MAXN],dfn[MAXN],dcnt;
array <int,3> rt,up;
vector <array<int,2>> b[MAXN];
void dfs0(int u) {vis[u]=true,dfn[u]=++dcnt;for(auto e:G[u]) if(!vis[e.v]) ok[e.i]=true,dfs0(e.v);
}
void dfs1(int u,int fz) {siz[u]=1,b[u].clear(),vis[u]=true,up=min(up,array<int,3>{dfn[u],u,0});for(auto e:G[u]) if(ok[e.i]&&vis[e.v]&&e.v!=fz) dfs1(e.v,u),siz[u]+=siz[e.v],b[u].push_back({siz[e.v],e.v});if(fz) b[u].push_back({siz[0]-siz[u],fz});sort(b[u].begin(),b[u].end());for(int i=0,s=0;i<(int)b[u].size();++i) {s+=b[u][i][0],rt=min(rt,array<int,3>{max(s+1,siz[0]-s),u,i});}
}
void dfs2(int u,int fz,vi&ord) {ord.push_back(u);for(auto e:G[u]) if(ok[e.i]&&vis[e.v]&&e.v!=fz) dfs2(e.v,u,ord);
}
int rk[MAXN];
void solve(int n,int m,vector<int>U,vector<int>V) {for(int i=0;i<m;++i) ++U[i],++V[i],G[U[i]].push_back({V[i],i+1}),G[V[i]].push_back({U[i],i+1});dfs0(1),memset(vis,false,sizeof(vis));vector<vi> S,nw; deque<int> id;auto ask=[&](int l,int r) {vector<int>qy(m);for(int i=0;i<n;++i) rk[id[i]]=i+1;for(int i=0;i<m;++i) {int x=rk[U[i]],y=rk[V[i]];qy[i]=(x<y)^(l<=min(x,y)&&max(x,y)<=r);}return query(qy);};S.push_back({});for(int i=1;i<=n;++i) S[0].push_back(i);while(true) {nw.clear(),id.clear();memset(inq,0,sizeof(inq));for(auto &q:S) {if(q.size()==1) continue;for(int u:q) vis[u]=true;rt=up={n+1,0,0},siz[0]=q.size(),dfs1(q[0],0); vi L,R;for(int i=0;i<(int)b[rt[1]].size();++i) dfs2(b[rt[1]][i][1],rt[1],(i<=rt[2]?L:R));if(count(R.begin(),R.end(),up[1])) swap(L,R);nw.push_back(L+rt[1]),nw.push_back(R+rt[1]);reverse(L.begin(),L.end()),L=L+rt[1]+R;int p=find(L.begin(),L.end(),up[1])-L.begin();if(!inq[up[1]]) for(int x:L) id.push_back(x),inq[x]=true;else {for(int i=p-1;~i;--i) id.push_front(L[i]),inq[L[i]]=true;for(int i=p+1;i<(int)L.size();++i) id.push_back(L[i]),inq[L[i]]=true;}for(int u:q) vis[u]=false;}if(ask(1,n)) break;reverse(id.begin(),id.end());if(ask(1,n)) break;S.swap(nw);}int L=1,R=n;while(true) {int mid=(L+R)>>1;if(ask(L,mid)) R=mid;else if(ask(mid+1,R)) L=mid+1;else {for(int i=1<<13;i;i>>=1) if(L+i<=mid&&ask(L+i,R)) L+=i;for(int i=1<<13;i;i>>=1) if(R-i>mid&&ask(L,R-i)) R-=i;break;}}answer(id[L-1]-1,id[R-1]-1);
}
N. [P11947] 可爱区间 (3)
Problem Link
考虑判断区间 \([l,r]\) 合法,很显然最优 \(c_i=a_i+(b_i-a_i)\times [l\le i\le r]\),需要满足以下条件:
- \(a[1,l)\) 所有后缀和非正,\(a(r,n]\) 所有前缀和非正。
- \(b[l,r]\) 所有前后缀和非正。
- \(a[1,l),a(r,n]\) 最大子段和 \(\le \sum_{l\le i\le r} b_i\)。
注意到第二个条件很强,记 \(b\) 前缀和为 \(s\),则相当于 \(s_{l-1},s_r\) 为 \(s[l-1,r]\) 的最小最大值。
那么枚举 \(l\),\(r\) 就是某个前缀最大值,则只要考虑 \(s_r\) 递增的若干 \(r\)。
\(s_{l-1}=\min s[l-1,r]\) 以及 \(a[1,l)\) 最大子段和 \(\le s_r-s_{l-1}\) 相当于 \(r\) 在某个区间中。
对称的 \(a(r,n]\) 的最大子段和 \(\le s_r-s_{l-1}\) 限制 \(r\) 在 \(l\) 小于某个阈值 \(v_r\) 的时候才合法,限制一是平凡的。
那么降序扫描 \(l\),动态维护 \(l\le v_r\) 且在单调栈中的所有 \(r\) 构成的集合 \(R\),那么左端点为 \(l\) 的合法右端点就是 \(R\) 的一个区间。
用历史和线段树变成单点加区间求一次历史和,查询就是差分后区间求和。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
#include "maxsum.h"
using namespace std;
const int MAXN=2.5e5+5,inf=2e9;
int n,m;
struct Segt {ll su[1<<19|5],tg[1<<19|5],hs[1<<19|5];void adt(int p,ll k) { hs[p]+=k*su[p],tg[p]+=k; }void psd(int p) { if(tg[p]) adt(p<<1,tg[p]),adt(p<<1|1,tg[p]),tg[p]=0; }void psu(int p) { hs[p]=hs[p<<1]+hs[p<<1|1],su[p]=su[p<<1]+su[p<<1|1]; }void upd(int x,int k,int l=1,int r=n,int p=1) {if(l==r) return su[p]=k,void();int mid=(l+r)>>1; psd(p);x<=mid?upd(x,k,l,mid,p<<1):upd(x,k,mid+1,r,p<<1|1);psu(p);}void add(int ul,int ur,int l=1,int r=n,int p=1) {if(ul<=l&&r<=ur) return adt(p,1);int mid=(l+r)>>1; psd(p);if(ul<=mid) add(ul,ur,l,mid,p<<1);if(mid<ur) add(ul,ur,mid+1,r,p<<1|1);psu(p);}ll qry(int ul,int ur,int l=1,int r=n,int p=1) {if(ul<=l&&r<=ur) return hs[p];int mid=(l+r)>>1; ll s=0; psd(p);if(ul<=mid) s+=qry(ul,ur,l,mid,p<<1);if(mid<ur) s+=qry(ul,ur,mid+1,r,p<<1|1);return s;}
} T;
int s1[MAXN],t1,s2[MAXN],t2,s3[MAXN],t3,cg[MAXN];
ll a[MAXN],b[MAXN],S[MAXN],zl[MAXN],zr[MAXN],pl[MAXN],pr[MAXN],ans[MAXN];
bool ins[MAXN];
vector <int> qy[MAXN],ad[MAXN];
vector<ll>maxsum(vector<int>A,vector<int>B,vector<int>L1,vector<int>R1,vector<int>L2,vector<int>R2) {n=A.size(),m=L1.size();for(int i=1;i<=n;++i) a[i]=A[i-1],b[i]=B[i-1],S[i]=S[i-1]+b[i];for(int i=1;i<=n;++i) pl[i]=max(0ll,pl[i-1])+a[i],zl[i]=max(zl[i-1],pl[i]);for(int i=n;i>=1;--i) pr[i]=max(0ll,pr[i+1])+a[i],zr[i]=max(zr[i+1],pr[i]);ll w1=-inf,w2=-inf;for(int i=1;i<=n;++i) w2=max(w2,min(w1,a[i])),w1=max(w1,a[i]);s3[++t3]=0;for(int i=1;i<=n;++i) {while(t3&&S[s3[t3]]>S[i]) --t3;if(pr[i+1]<=0&&t3&&S[i]-S[s3[1]]>=zr[i+1]) {int p=1;for(int k=1<<18;k;k>>=1) if(p+k<=t3&&S[i]-S[s3[p+k]]>=zr[i+1]) p+=k;ad[s3[p]].push_back(i);}s3[++t3]=i;}s1[0]=s2[0]=n+1,s1[1]=s2[1]=n,t1=t2=1,ins[n]=true;for(int i=0;i<m;++i) qy[R1[i]+1].push_back(i),qy[L1[i]].push_back(i);for(int i=n-1;i>=0;--i) {if(b[i+1]<0&&b[i+1]>=(a[i+1]==w1?w2:w1)) T.upd(i+1,1),T.add(i+1,i+1),T.upd(i+1,0);while(t1&&S[s1[t1]]<S[i]) ins[s1[t1]]=false,T.upd(s1[t1--],0);while(t2&&S[s2[t2]]>=S[i]) --t2;for(int x:ad[i]) if(ins[x]) T.upd(x,1);if(pl[i]<=0&&t1&&s1[t1]<s2[t2]&&S[s1[1]]-S[i]>=zl[i]) {int l=t1,r=1;for(int k=1<<18;k;k>>=1) {if(l-k>=1&&s1[l-k]<s2[t2]) l-=k;if(r+k<=t1&&S[s1[r+k]]-S[i]>=zl[i]) r+=k;}if(l<=r) T.add(s1[r],s1[l]);}s1[++t1]=i,s2[++t2]=i,ins[i]=true;for(int x:qy[i]) {if(i==L1[x]) ans[x]+=T.qry(L2[x]+1,R2[x]+1);else ans[x]-=T.qry(L2[x]+1,R2[x]+1);}}return vector<ll>(ans,ans+m);
}
O. [P12019] 洪水 (2.5)
Problem Link
停止时被淹没的区域必定是一个矩形。
因此找到所有边界都是 \(1\) 的矩形,求覆盖每个点的最小矩形。
枚举矩形下边界,动态维护每列的最大高度 \(h\),再扫描线右边界 \(r\),注意到两个有交的矩形可以不用加入,只要加入它们边界构成的子矩形。
因此类似支配对的理论,只要考虑插入 \(h_r\) 时被弹出的后缀最大值单调栈栈顶(以及最终栈顶)作为左边界 \(l\)。
查询 \((l,r)\) 对应的上边界,可以对每个 \(r\) 维护所有 \(l\) 上一次连通的行,可以用 BIT 加速计算。
然后求覆盖每个点的最小矩形是经典问题,排序后简单线段树即可。
时间复杂度 \(\mathcal O(nm\log m)\)。
我的最小覆盖矩形部分是暴力,但能通过官方数据。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005;
int n,m,a[MAXN][MAXN];
struct FenwickTree {int tr[MAXN],z;void upd(int x,int v) { for(;x<=m;x+=x&-x) tr[x]=v; }int qry(int x) { for(z=0;x;x&=x-1) z=max(z,tr[x]); return z; }
} T[MAXN];
int up[MAXN],st[MAXN],tp,dsu[MAXN][MAXN];
int find(int *f,int x) { return f[x]^x?f[x]=find(f,f[x]):x; }
long long ans=0;
void upd(int *f,int l,int r,int z) {for(int i=find(f,l);i<=r;f[i]=i+1,i=find(f,i)) ans+=z;
}
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>m,n+=2,m+=2;for(int i=1;i<=n;++i) a[i][1]=a[i][m]=1;for(int i=1;i<=m;++i) a[1][i]=a[n][i]=1;for(int i=2;i<n;++i) for(int j=2;j<m;++j) {char o; cin>>o,a[i][j]=o-'0';}for(int i=1;i<=m;++i) up[i]=1,T[i].upd(1,1);basic_string<array<int,5>> F;for(int t=2;t<=n;++t) {auto link=[&](int l,int r,int q) {if(r-l<2||l<1||r>m) return ;int k=T[r-1].qry(l+1);if(t-k<2||(l<q&&q<r&&k>=up[q]-1)) return ;if(k>=max(up[l],up[r])-1) F.push_back({(t-k-1)*(r-l-1),k,t,l,r});};for(int l=1,r;l<=m;++l) if(a[t][l]) {for(r=l;r<m&&a[t][r+1];++r);st[tp=0]=l-1;for(int i=l;i<=r;++i) {for(;tp&&up[st[tp]]>up[i];--tp) link(st[tp],i,st[tp+1]);link(st[tp],i,st[tp+1]),st[++tp]=i;}for(int i=0;i<=tp;++i) link(st[i],r+1,st[tp+1]);for(int i=l;i<=r;++i) T[i].upd(l,t);l=r;}for(int i=1;i<=m;++i) if(!a[t][i]) up[i]=t+1;}for(int i=1;i<=n;++i) {iota(dsu[i]+1,dsu[i]+m+2,1);for(int j=1;j<=m;++j) if(a[i][j]) dsu[i][j]=j+1;}stable_sort(F.begin(),F.end());for(auto o:F) {for(int i=o[1]+1;i<=o[2]-1;++i) upd(dsu[i],o[3]+1,o[4]-1,o[0]);}cout<<ans<<"\n";return 0;
}
*P. [P11994] 外郎糕 (7)
Problem Link
首先选择 \(x\) 则后面所有 \(\le x\) 的数都要选,那么每种值选的是一个后缀且长度递减。
我们需要进一步刻画性质,不妨考虑值域为 \(2\) 的情况,那么我们猜测做法一定是优先选尽可能多的 \(1\)。
反证法,如果当前最后一个未选的 \(1\) 为 \(x\),首个选了的 \(2\) 为 \(y\),那么把 \(y\) 换成 \(x\),很显然 \([x,y)\) 部分中只选了 \(1\),必定合法,而 \([y,n]\) 部分也显然合法。
同理可以推广到值域更大的情况,我们只要按值域从小到大贪心取即可。
实现的时候可以用 ST 表快速判定然后二分长度,精细实现后可以使得 ST 表总长度之和 \(=n\)。
时间复杂度 \(\mathcal O((n+q\log n)V)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
const ll inf=1e18;
int n,m,a[MAXN],L[MAXN],R[MAXN],p[MAXN],ans[MAXN];
ll s[MAXN],b[MAXN],c[MAXN],w[MAXN],st[18][MAXN],mn[MAXN];
int pr[MAXN],id[MAXN],k;
ll qmn(int l,int r) {if(l>r) return inf;int j=__lg(r-l+1);return min(st[j][l],st[j][r-(1<<j)+1]);
}
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n;for(int i=1;i<=n;++i) cin>>a[i],s[i]=s[i-1]+a[i];cin>>m;for(int i=1;i<=m;++i) cin>>L[i]>>R[i],p[i]=L[i]-1;for(int z=1;z<=100;++z) {k=0,mn[0]=inf;for(int i=1;i<=n;++i) {if(a[i]==z) id[++k]=i;pr[i]=k,b[i]=b[i-1]+(a[i]<=z?-a[i]:a[i]);c[i]=c[i-1]+(a[i]<z?-a[i]:a[i]);mn[i]=min(b[i],a[i]==z?inf:mn[i-1]);}if(!k) continue;id[k+1]=n+1;for(int i=1;i<=k;++i) st[0][i]=mn[id[i+1]-1];for(int j=1;j<18;++j) for(int i=1;i+(1<<j)-1<=k;++i) st[j][i]=min(st[j-1][i],st[j-1][i+(1<<(j-1))]);for(int i=1;i<=m;++i) {int hd=pr[p[i]]+1,x=pr[R[i]]+1,tl=x-1;auto chk=[&](int x) -> bool {return min(qmn(x,tl-1),mn[R[i]])-b[id[x]-1]+w[i]+c[id[x]-1]-c[p[i]]>=0;};if(x>hd) for(int j=1<<__lg(x-hd);j;j>>=1) if(x-j>=hd&&chk(x-j)) x-=j,ans[i]+=j;if(x>hd) w[i]+=c[id[x-1]]-c[p[i]],p[i]=id[x-1];}}for(int i=1;i<=m;++i) cout<<ans[i]<<"\n";return 0;
}
Q. [P11991] 多方通信(1)
Problem Link
\(n=32\) 容易想到类似线段树的结构,即第 \(k=1\sim 4\) 次询问答案是否在该点的 \(k\) 级祖先子树内。
然后能找到答案在哪个子树内,倒过来向下重复找到答案即可。
\(n=48\) 就在分成三个大小为 \(16\) 的子树,然后每个点询问后一个子树就能确定答案。
时间复杂度 \(\mathcal O(n^2\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int n; cin>>n;int k=n==4?1:4;cout<<2*k+(n>(2<<k))<<"\n";for(int p=0;p<n;++p) {cout<<p+1<<"\n";for(int i=0;i<n;++i) {for(int j=0;j<k;++j) {cout<<"FT"[(p>>j)==(i>>j)]<<" "<<(i^(1<<j))+1<<" ";}if(n>(2<<k)) cout<<"FT"[(p>>k)==(i>>k)]<<" "<<(i+(1<<k))%n+1<<" ";for(int j=k-1;~j;--j) {cout<<"FT"[(p>>j)==(i>>j)]<<" "<<(p>>(j+1)<<(j+1)|(i&((1<<(j+1))-1)))+1<<" ";}cout<<"\n";}}return 0;
}
R. [P10684] 分割(4)
Problem Link
蓝球看成 0,红球看成 1。
首先不进行列交换时,根据经典 01 序列逆序对数的结论,操作次数可以表示成 \(S-\dfrac{c_1(c_1+1)}2-\dfrac{c_2(c_2+1)}2\),其中 \(c_1,c_2\) 是每行 \(0\) 个数,\(S\) 是每个 \(0\) 的下标之和。
那么每次竖向交换不会影响 \(S\),但会改变某个 \(c\)。
注意到 \(c_1+c_2\) 一定,\(c_1-c_2\) 越大越好,因此 \(a_i=(1,0)\) 的 \(i\) 一定会直接交换。
此时一定有 \(c_1\ge c_2\),然后还有若干 \(a_i=(0,0)\) 的列,我们要把它们匹配一些 \(a_j=(1,1)\) 的列。
注意 \(i\to j\) 的过程会改变 \(S\),因此代价为 \(1+2\max(0,j-i)\)。
枚举交换次数 \(k\),可以计算每一条边 \((i,i+1)\) 的贡献,容易发现 \(k\) 不超过 \([1,i]\) 中 \((1,1)\) 个数加上 \([i+1,n]\) 中 \((0,0)\) 个数时无贡献,此后每次贡献 \(+2\)。
因此可以简单维护做到 \(\mathcal O(nq)\)。
进一步观察每个 \(c_1\) 对应的代价,可以发现每次操作的影响只有简单的区间加,可以线段树维护。
时间复杂度 \(\mathcal O(q\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5;
const ll inf=1e18;
int n,m,a[MAXN][2],L,R,K,c0,b[MAXN];
ll S,f[MAXN];
ll s2(ll x) { return x*(x+1)/2; }
struct Segt {ll tr[1<<21|5],ad[1<<21|5];void adt(int p,ll k) { tr[p]+=k,ad[p]+=k; }void psd(int p) { if(ad[p]) adt(p<<1,ad[p]),adt(p<<1|1,ad[p]),ad[p]=0; }void psu(int p) { tr[p]=min(tr[p<<1],tr[p<<1|1]); }void init(int l=L,int r=R,int p=1) {if(l==r) return tr[p]=f[l],void();int mid=(l+r)>>1;init(l,mid,p<<1),init(mid+1,r,p<<1|1);psu(p);}void add(int ul,int ur,int k,int l=L,int r=R,int p=1) {if(ul<=l&&r<=ur) return adt(p,k);int mid=(l+r)>>1; psd(p);if(ul<=mid) add(ul,ur,k,l,mid,p<<1);if(mid<ur) add(ul,ur,k,mid+1,r,p<<1|1);psu(p);}ll qry(int ul,int ur,int l=L,int r=R,int p=1) {if(ul<=l&&r<=ur) return tr[p];int mid=(l+r)>>1; ll s=inf; psd(p);if(ul<=mid) s=min(s,qry(ul,ur,l,mid,p<<1));if(mid<ur) s=min(s,qry(ul,ur,mid+1,r,p<<1|1));return s;}
} T;
void add(int x,int k) {if(k>0) ++b[x];if(b[x]<=R) T.add(max(b[x],L),R,-2*k);if(k<0) --b[x];
}
ll solve() { return S-c0+T.qry(K,R); }
signed main() {ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);cin>>n>>m;for(int j:{0,1}) for(int i=1;i<=n;++i) {char o; cin>>o,a[i][j]=o=='C';if(!a[i][j]) S+=i;}for(int i=1;i<=n;++i) {c0+=!a[i][0];K+=(!a[i][0]||!a[i][1]);R+=1-a[i][0]+1-a[i][1];}b[n]=n;for(int i=n;i>1;--i) {b[i-1]=b[i];if(a[i][0]==a[i][1]) b[i-1]+=a[i][0]?-1:1;if(b[i-1]<n) f[b[i-1]+1]+=2;}for(int i=1;i<=n;++i) f[i]+=f[i-1];for(int i=1;i<=n;++i) f[i]+=f[i-1];if(R>n) L=R-n,R=n;for(int i=L;i<=R;++i) f[i]-=s2(i-1)+s2(L+R-i);T.init();cout<<solve()<<"\n";for(int o,x,y;m--;) {cin>>o>>y>>x,--y,--o;if(a[x][y]==a[x+1-o][y+o]) { cout<<solve()<<"\n"; continue; }if(o) c0+=a[x][0]?1:-1,swap(a[x][0],a[x][1]);else {if(a[x][y^1]!=a[x+1][y^1]) K+=(a[x][y^1]==a[x][y]?1:-1);add(x,a[x][y]?-1:1),S+=a[x][y]?-1:1;swap(a[x][y],a[x+1][y]);}cout<<solve()<<"\n";}return 0;
}
S. [P11992] 电路 2 (5.5)
Problem Link
首先一条链可以尝试二分找第一个 OR,即选一个前缀,每个点挂的叶子填 \(1\),链下方的点填 \(0\),那么可以判断前缀中是否有 OR,找到 OR 后,可以同时翻转该点以及其两个儿子使其变成 AND,然后就能继续二分了。
然后考虑完美二叉树的情况,那么依旧要二分,发现我们可以从上往下对每层的点二分,具体来说之前每层的点已经知道符号,所有可以全部变成 OR,然后这层要检验的每个点一个子树全填 \(0\),一个子树全填 \(1\),就能判断这些点中有没有 OR。
一般的情况考虑拼合两种构造,我们可以选若干条没有祖先后代关系的链,要求这些点的祖先都已经被确定,此时就能在这个点集上二分了。
具体就是每条链用第一种方法构造,使得有 OR 时链顶为 \(1\),然后每个点的所有祖先填 OR 即可。
此时我们把图分成 \(k\) 个点集就会使用 \(k\) 次额外的询问,注意到取出原树的重链剖分,按到根轻边个数分组恰好满足题意,则交互次数 \(R(\log n+1)+\log n\)。
注意到瓶颈在第一部分,这种二分求多个物品的问题,一个经典优化就是顶层分块,我们取块长 \(2^b\),然后把每个点集拆成若干 \(\le 2^b\) 的段,然后每段内部依次二分,则交互次数 \(R(b+1)+\log n+\dfrac{n}{2^b}\),取 \(b=6\) 可以通过过。
时间复杂度 \(\mathcal O(n^2)\)。
代码:
#include<bits/stdc++.h>
#include "circuit.h"
using namespace std;
const int MAXN=16005,B=64;
basic_string <int> G[MAXN],ch[MAXN],id[MAXN],E[MAXN];
int n,siz[MAXN],hson[MAXN];
void dfs1(int u) {siz[u]=1,hson[u]=n;for(int v:G[u]) {dfs1(v),siz[u]+=siz[v];if(siz[v]>siz[hson[u]]) hson[u]=v;}
}
void dfs2(int u,int d) {ch[d].push_back(u);if(hson[u]<n) dfs2(hson[u],d);for(int v:G[u]) if(v^hson[u]) dfs2(v,d+1);
}
string solve(int N,int,vector<int>L,vector<int>R) {for(int i=0;i<N;++i) for(int x:{L[i],R[i]}) if(x<N) G[i].push_back(x);for(int i=N;i<=2*N;++i) E[i]={i};for(int i=N-1;~i;--i) E[i]=E[L[i]]+E[R[i]];n=N,dfs1(0),dfs2(0,0);int m=0;for(int d=0;d<20;++d) if(ch[d].size()) {for(int x:ch[d]) {id[m].push_back(x);if((int)id[m].size()==B) ++m;}m++;}string ans=string(n,'&');for(int d=0;d<m;++d) if(id[d].size()) {string qy=string(2*n+1,'0');for(int c=0;c<d;++c) for(int x:id[c]) {if(ans[x]=='&') qy[x]^=1,qy[L[x]]^=1,qy[R[x]]^=1;}auto chk=[&](int x) {string t=qy;for(int i=0;i<=x;++i) {int u=id[d][i];if(hson[u]<n) {for(int o:E[L[u]^R[u]^hson[u]]) t[o]^=1;} else t[L[u]]^=1;}return query(t);};int sz=id[d].size()-1,p=-1;while(chk(sz)) {int x=sz;for(int k=1<<15;k;k>>=1) if(x-k>p&&chk(x-k)) x-=k;int u=id[d][x]; p=x;ans[u]='|',qy[u]^=1,qy[L[u]]^=1,qy[R[u]]^=1;}}return ans;
}
T. [P11989] 勇者比太郎 3 (6)
Problem Link
首先难度确定是就是朴素贪心,每个时刻弹出最大值可以做到 \(\mathcal O(n\log n)\)。
但另一种等价的做法就是按 \(p\) 从大到小考虑每个怪兽,尽可能向后一直打该怪兽,用并查集维护非空段,时间复杂度 \(\mathcal O(n)\)。
可以猜测难度在 \(1\sim L\) 的变化过程中有很多时刻变化量相同,我们生成答案关于难度的函数是 \(\mathcal O(n)\) 段的凸壳。
那么我们可以每次二分找到当前段的范围,可以做到 \(\mathcal O(n^2\log L)\),回答可以双指针 \(\mathcal O(q+L)\)。
但是这无法通过,进一步观察,如果难度 \(k-1\to k,k\to k+1\) 变化量相同,那么可以猜测每种怪兽被攻击次数变化量相同,那么每个怪兽被打的次数是一次函数,我们检验的时候只要判断每个后缀中点数是否不超过后缀大小,可以直接计算出最大合法时刻,所以可以 \(\mathcal O(n)\) 求出斜率区间。
需要精细实现。
时间复杂度 \(\mathcal O(n^2+q+L)\)。
正解是把答案拆成对每个 \(k\) 把权值看成 \([p_i\ge k]\) 求和,变成最大匹配后用 Hall 定理刻画。
代码:
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
namespace IO {
int ow,olim=(1<<21)-100;
char buf[1<<21],*p1=buf,*p2=buf,obuf[1<<21];
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
ll read() {ll x=0; char c=gc();while(!isdigit(c)) c=gc();while(isdigit(c)) x=x*10+(c^48),c=gc();return x;
}
void flush() {fwrite(obuf,ow,1,stdout),ow=0;
}
void write(ll x) {if(!x) obuf[ow++]='0';else {int t=ow;for(;x;x/=10) obuf[ow++]=(x%10)^48;reverse(obuf+t,obuf+ow);}if(ow>=olim) flush();
}
void putc(char c) {obuf[ow++]=c;if(ow>=olim) flush();
}
#undef gc
}
const int MAXN=6005,MAXM=1e7+5;
const ll inf=2e18;
struct item {ll t,h,w;
} a[MAXN];
int n,m,q,dsu[MAXN],id[MAXN];
ll f[MAXM],g[MAXN],t[MAXN],h[MAXN];
ull sg[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
int qry(int x) {int y=x>>6,r=x&63;if(sg[y]>>r) return __builtin_ctzll(sg[y]>>r)+x;y=find(y+1);return y<<6|__builtin_ctzll(sg[y]);
}
void sol(ll k) {int b=(n>>6)+1;for(int i=0;i<=b;++i) dsu[i]=i,sg[i]=-1;for(int i=1;i<=n;++i) g[i]=a[i+1].t-a[i].t;for(int o=1,i;o<=n;++o) {ll c=a[i=id[o]].h*k;for(int x=qry(i),y;x<=n&&c;) {if(c>=g[x]) {c-=g[x],g[x]=0,sg[y=(x>>6)]^=1ll<<(x&63);if(!sg[y]) dsu[y]=y+1;x=qry(x);}else g[x]-=c,c=0;}h[i]=a[i].h*k-c;}
}
signed main() {n=IO::read(),m=IO::read(),a[n+1].t=IO::read();ll S=0;for(int i=1;i<=n;++i) a[i].t=IO::read(),a[i].h=IO::read(),a[i].w=IO::read(),S+=a[i].h*a[i].w,id[i]=i;sort(a+1,a+n+1,[&](auto x,auto y) { return x.t<y.t; });sort(id+1,id+n+1,[&](int x,int y){ return a[x].w>a[y].w; });for(int p=1;p<=m;) {sol(p),f[p]=0;for(int i=1;i<=n;++i) f[p]+=h[i]*a[i].w;ll k=0,b=0,x=m-p,y=1;auto upd=[&](ll u,ll v) { if((__int128)u*y<(__int128)x*v) x=u,y=v; };for(int i=n;i>=1;--i) {b+=a[i+1].t-a[i].t;k+=h[i]-t[i],b-=h[i];if(k>0) upd(b,k);if(h[i]-t[i]>a[i].h) upd(a[i].h*p-h[i],h[i]-t[i]-a[i].h);else if(h[i]-t[i]<0) upd(h[i],t[i]-h[i]);if(x<y) break;}ll d=x/y;for(int i=p+1;i<=p+d;++i) f[i]=f[i-1]+f[p]-f[p-1];for(int i=1;i<=n;++i) t[i]=h[i]+d*(h[i]-t[i]);p+=d+1;}f[m+1]=inf;for(int i=m;~i;--i) f[i]=min(f[i+1],S*i-f[i]);q=IO::read();for(int i=0;q--;) {ll z=IO::read();while(i<m&&f[i+1]<=z) ++i;IO::write(i),IO::putc('\n');}IO::flush();return 0;
}