JOISC2017 题解
\(\text{By DaiRuiChen 007}\)
A. Cultivation
题目大意
在一个 \(r\times c\) 的网格上有 \(n\) 个格子是黑色的。
每次操作可以把所有黑色格子向上下左右的某个方向扩展一格,即把所有黑色格子的左侧染成黑色(其他三个方向同理)。
求至少需要几次操作染黑整个网格。
数据范围:\(r,c\le 10^9,n\le 300\)。
思路分析
算法一
爆搜,时间复杂度 \(\mathcal O(4^{R+C})\)。
算法二
注意到每个节点在经过若干次操作后一定会扩展出一个矩形,可以证明无论如何调换操作顺序,最终的矩形形状不变,因此我们得到了一个重要结论:最终扩展出的图形只和上下左右操作的次数有关系,而和其相对顺序无关。
因此枚举四种操作分别进行的次数 \(u,d,l,r\) 然后暴力验证即可,时间复杂度 \(\mathcal O(R^3C^3)\)。
算法三
先考虑 \(R=1\) 的情况,此时把所有黑色色方格所在列排序得到 \(c_1,c_2,\dots,c_m\),那么我们可以把所有的格子分成三类:
- 在最左侧黑格外:\(i<c_1\),为了保证这些格子被染色应该有 \(l\ge c_1-1\)。
- 在两个黑格之间:\(c_1\le i\le c_m\),为了保证这些格子被染色应该有 \(l+r\ge \max\{c_{i+1}-c_1-1\}\)。
- 在最右侧黑格外:\(c_m<i\),为了保证这些格子被染色应该有 \(r\ge C-c_m\)。
综上,\(l+r\ge\max\{\max\{c_{i+1}-c_i-1\},c_1-1+C-c_m\}\)。
因此我们可以在 \(\mathcal O(n\log n)\) 的时间复杂度内解决 \(R=1\) 的情况,瓶颈在于对 \(c_i\) 排序。
回到原问题,我们同样可以枚举 \(u,d\),然后对于每一行求出哪些点染到了这一行,用上面的方法分别处理出关于 \(l,r\) 的限制关系式,然后最后把所有限制关系式联立既可,时间复杂度 \(\mathcal O(R^3n\log n)\)
算法四
观察到对于每一行,染到这一行的点按 \(r_i\) 排序后是连续的,因此直接预处理出每个区间的答案即可。
进一步观察发现对于每一对 \((u,d)\),其本质不同的行只有 \(\mathcal O(n)\) 个,这些行对应的区间可以用滑动窗口直接维护。
因此我们把所有 \(r_i-u,r_i+d\) 离散化,然后用滑动窗口即可快速求出答案,时间复杂度 \(\mathcal O(R^2n)\)。
注意到如果提前把所有 \(r_i\) 排序,那么离散化可以看作对两个有序序列归并排序,因此可以优化掉排序的 \(\mathcal O(n\log n)\) 复杂度。
算法五
考虑 \(u\gets u-1,d\gets d+1\) 的过程,容易发现我们可以把这个过程看做在无限大的地图中整个网格向下移动了一行。那么,对于所有 \(u+d\) 一定的 \((u,d)\),我们可以把枚举 \(u,d\) 的过程看成用一个宽为 \(R\) 的窗口扫整个网格的过程。
因此我们枚举 \(u+d\) 的和 \(S\),然后对 \(r_i,r_i+S\) 离散化,求出每一行对应的 \(l,r,l+r\) 的下界,然后用一个宽为 \(R\) 的窗口在每一行上扫,扫的时候用单调队列维护区间对 \(l,r,l+r\) 下界限制的最大值,最后统一计算答案即可。
时间复杂度 \(\mathcal O(Rn)\)。
算法六
观察到很多 \(S\) 其实是是在做无效枚举,只有当 \(S\) 变大能影响每一行的形态时,我们才需要考虑这样的 \(S\)
可以证明此时要么 \(u=r_i-1\) 此时第一行多一个 \(i\),要么 \(d=R-r_i\) 此时第 \(R\) 行多一个 \(i\),要么 \(S=r_{j}-r_{i}-1\),此时中间 \(i,j\) 相交,容易证明不存在其他取法影响行的形态。
当然我们要计算出 \(S\) 的下界以保证每一行都非空,注意到这样的 \(S\) 是 \(\mathcal O(n^2)\) 级别的,因此直接枚举并用算法五的方法计算即可。
时间复杂度 \(\mathcal O(n^3)\),可以通过此题。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=301,INF=1e18;
int R,C,n,ans=INF;
struct Point {
int r,c;
} a[MAXN];
struct Info {
int lo,hi,gap;
Info(): lo(INF),hi(INF),gap(INF) {}
} f[MAXN][MAXN];
struct RMQ_Queue {
int q[MAXN<<1],tim[MAXN<<1],val[MAXN<<1],head,tail,siz;
RMQ_Queue() {
head=1,tail=0,siz=0;
memset(q,0,sizeof(q)),memset(tim,0,sizeof(tim)),memset(val,0,sizeof(val));
}
inline void insert(int ti,int v) {
++siz,tim[siz]=ti,val[siz]=v;
while(head<=tail&&val[q[tail]]<=v) --tail;
q[++tail]=siz;
}
inline void erase(int ti) {
while(head<=tail&&tim[q[head]]<ti) ++head;
}
inline int qmax() {
assert(head<=tail);
return val[q[head]];
}
};
RMQ_Queue Lo,Hi,Gap;
unordered_map <int,int> rem;
inline int solve(int len) {
if(rem.find(len)!=rem.end()) return rem[len];
vector <int> rows;
for(int i=1;i<=n;++i) rows.push_back(a[i].r);
for(int i=1;i<=n;++i) rows.push_back(a[i].r+len+1); //[a[i].r,a[i].r+len+1)
inplace_merge(rows.begin(),rows.begin()+n,rows.end());
rows.erase(unique(rows.begin(),rows.end()),rows.end());
int m=rows.size()-1;
vector <Info> sec(m);
for(int i=0,l=1,r=0;i<m;++i) {
while(l<=n&&a[l].r+len+1<=rows[i]) ++l;
while(r<n&&a[r+1].r<=rows[i]) ++r;
assert(l<=r); sec[i]=f[l][r];
}
Lo=RMQ_Queue(),Hi=RMQ_Queue(),Gap=RMQ_Queue();
int ret=INF;
for(int i=0,p=-1;i<m;++i) {
if(rows[i]+R-1>=rows[m]) break;
Lo.erase(rows[i]),Hi.erase(rows[i]),Gap.erase(rows[i]);
while(p+1<m&&rows[p+1]<=rows[i]+R-1) {
++p;
Lo.insert(rows[p],sec[p].lo);
Hi.insert(rows[p],sec[p].hi);
Gap.insert(rows[p],sec[p].gap);
}
ret=min(ret,max(Gap.qmax(),Lo.qmax()+Hi.qmax()));
}
return rem[len]=ret;
}
signed main() {
scanf("%lld%lld%lld",&R,&C,&n);
for(int i=1;i<=n;++i) scanf("%lld%lld",&a[i].r,&a[i].c);
sort(a+1,a+n+1,[&](Point u,Point v){ return u.r<v.r; });
for(int l=1;l<=n;++l) {
vector <int> cols;
for(int r=l;r<=n;++r) {
cols.insert(upper_bound(cols.begin(),cols.end(),a[r].c),a[r].c);
f[l][r].lo=cols.front()-1,f[l][r].hi=C-cols.back(),f[l][r].gap=0;
for(int i=1;i<(int)cols.size();++i) f[l][r].gap=max(f[l][r].gap,cols[i]-cols[i-1]-1);
}
}
int lb=(a[1].r-1)+(R-a[n].r);
for(int i=1;i<n;++i) lb=max(lb,a[i+1].r-a[i].r-1);
for(int i=1;i<=n;++i) {
for(int j=1;j<=n;++j) {
int len=(a[i].r-1)+(R-a[j].r);
if(len>=lb&&len<ans) ans=min(ans,len+solve(len));
}
}
for(int i=1;i<=n;++i) {
for(int j=i;j<=n;++j) {
int len=a[j].r-a[i].r-1;
if(len>=lb&&len<ans) ans=min(ans,len+solve(len));
}
}
printf("%lld\n",ans);
return 0;
}
B. Port Facility
题目大意
有 \(n\) 个物品和两个栈(后进先出),每个物品可能进入其中某个栈,现已知 \(n\) 个物品的进出栈顺序,求有多少种安排物品入栈出栈的方式满足顺序要求。
数据范围:\(n\le 10^6\)。
思路分析
假如我们把每个栈的入栈出栈时间看成一个线段的话,那么同一个栈中的线段要么不相交要么包含,因此不能在同一个栈中的物品可以写成若干组关于 \((l,r)\) 的二维偏序关系。
假如我们把不能在同一个栈中的物品相连,那么这就是一个统计 2-SAT 解数的问题,若原图是二分图则答案为 \(2^b\),\(b\) 为连通块个数,否则答案为 \(0\)。
注意到题目中的二维偏序限制关系可以用主席树优化建图,然后用扩域并查集统计答案。
下面简单讲一下如何实现解题目中的 2-SAT:
- 首先建出主席树,把每个线段挂到对应的主席树的叶子节点上,并把这两个节点设为同色。
- 对于每个二维偏序限制,从对应线段节点连到主席树某个区间节点上,并把这两个节点设为异色。
- 把所有被连边的主席树区间节点取出,将这些节点的子树全部设为同色。
前两步并查集就可以直接做,而第三步需要离线出所有节点再 BFS 一遍以保证复杂度。
时间复杂度 \(\mathcal O(n\log n\alpha(n))\),注意实现常数。
代码呈现
#include<bits/stdc++.h>
#pragma GCC optimize("Ofast")
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
inline int read(){
int x=0; char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
return x;
}
using namespace std;
const int MAXN=1e6+1,MAXV=MAXN*24,MOD=1e9+7;
int n,siz;
struct Node {
int ls,rs;
} tree[MAXV];
int tar[MAXN];
inline void Append(int id,int u,int l,int r,int src,int &des) {
tree[des=++siz]=tree[src];
if(l==r) { tar[id]=des; return ; }
int mid=(l+r)>>1;
if(u<=mid) Append(id,u,l,mid,tree[src].ls,tree[des].ls);
else Append(id,u,mid+1,r,tree[src].rs,tree[des].rs);
}
vector <int> sec[MAXN];
inline void Link(int u,int ul,int ur,int l,int r,int pos) {
if(ul>ur||!pos) return ;
if(ul<=l&&r<=ur) { sec[u].push_back(pos); return ; }
int mid=(l+r)>>1;
if(ul<=mid) Link(u,ul,ur,l,mid,tree[pos].ls);
if(mid<ur) Link(u,ul,ur,mid+1,r,tree[pos].rs);
}
struct Interval {
int l,r;
} a[MAXN];
int root[MAXN],dsu[MAXV<<1],rnk[MAXV<<1];
inline int find(int x) {
int u=x,fa;
while(dsu[u]!=u) u=dsu[u];
while(x!=u) fa=dsu[x],dsu[x]=u,x=fa;
return u;
}
inline void merge(int u,int v) {
u=find(u),v=find(v);
if(u==v) return ;
if(rnk[u]<rnk[v]) swap(u,v);
dsu[v]=u,rnk[u]+=(rnk[u]==rnk[v]);
}
bool vis[MAXV],inq[MAXV<<1];
signed main() {
siz=n=read();
vector <int> rp;
for(int i=1;i<=n;++i) a[i].l=read(),a[i].r=read(),rp.push_back(a[i].r);
sort(a+1,a+n+1,[&](Interval u,Interval v) { return u.l<v.l; });
sort(rp.begin(),rp.end());
for(int i=1;i<=n;++i) {
int lid=lower_bound(rp.begin(),rp.end(),a[i].l)-rp.begin()+1;
int rid=lower_bound(rp.begin(),rp.end(),a[i].r)-rp.begin()+1;
Link(i,lid,rid-1,1,n,root[i-1]);
Append(i,rid,1,n,root[i-1],root[i]);
}
iota(dsu+1,dsu+siz*2+1,1);
fill(rnk+1,rnk+siz*2+1,1);
auto equal=[&](int u,int v) {
merge(u,v),merge(u+siz,v+siz);
if(find(u)==find(u+siz)||find(v)==find(v+siz)) puts("0"),exit(0);
};
auto diff=[&](int u,int v) {
merge(u,v+siz),merge(u+siz,v);
if(find(u)==find(u+siz)||find(v)==find(v+siz)) puts("0"),exit(0);
};
queue <int> Q;
for(int i=1;i<=n;++i) {
diff(tar[i],i);
for(int u:sec[i]) equal(i,u),Q.push(u),vis[u]=true;
}
while(!Q.empty()) {
int u=Q.front(); Q.pop();
if(u<=n) continue;
for(int v:{tree[u].ls,tree[u].rs}) if(v) {
equal(u,v);
if(!vis[v]) Q.push(v),vis[v]=true;
}
}
int ans=1;
for(int i=1,i0,i1;i<=n;++i) {
i0=find(i),i1=find(i+siz);
if(i0==i1) {
puts("0");
return 0;
} else if(!inq[i0]&&!inq[i1]) {
ans=ans*2%MOD;
inq[i0]=inq[i1]=true;
}
}
printf("%d\n",ans);
return 0;
}
C. Sparklers
题目大意
数轴上有 \(n\) 个人,其位置分别为 \(x_1,x_2,\dots,x_n\),开始时,\(k\) 号手中的烟花处于刚被点亮状态,若一个烟花不被点亮的人 \(i\) 和一个烟花被点亮的的人到 \(j\) 达同一位置后,\(i\) 手中的烟花会被点亮
每个点亮的烟花可以持续燃烧 \(T\) 秒,每个人可以以任意非负整数的速度移动,求所有人的最大速度至少是多少才能使每个人至少被点亮一次
数据范围:\(n\le 10^5\)。
思路分析
显然本题答案具有可二分性,二分一个最大速度 \(v\) 之后可以让所有人都以 \(v\) 的速度移动。
考虑刻画点燃烟花棒的过程,显然某个时刻场上有两个点燃的烟花一定不优于把后点燃的一个留住,跟着先点燃的那个人跑,直到先点燃的那个人燃烧结束时再传火,可以证明这样一定更优。
一个显然的观察是:对于任意时刻,点燃过烟花的人一定是一个包含 \(k\) 的区间 \([l,r]\),并且每个人都会向 \([l,r]\) 靠近然后在区间终点依次传火,因此我们得到判定条件:区间 \(\mathbf{[l,r]}\) 中的所有人都能被点燃烟花的充分非必要条件为 \(\mathbf{(r-l)T\ge\dfrac{x_r-x_l}{2v}}\),事实上,这个条件描述的是保证 \([l,r]\) 区间中最后一个被点燃烟花的人合法。
令 \(e_i=x_i-2\times v\times T\times i\),根据归纳法,那么区间 \([l,r]\) 合法当且仅当 \(e_l\ge e_r\) 且区间 \((l,r],[l,r)\) 中有一个合法。
因此我们需要从 \([k,k]\) 区间不断拓展,直到拓展到 \([1,n]\),考虑某个时刻我们让 \([l,r]\gets [l-1,r]\),若 \(e_{l-1}<e_l\),那么我们继续拓展 \(l\) 一定不劣,因为此时拓展 \(r\to r'\) 的由于 \(e_l>e_{l-1}\ge e_{r'}\),可以在拓展 \(l\) 之前先做。
因此每次我们拓展 \([l,r]\) 到 \([l',r]\) 都会使得 \(e_{l'}\ge e_l\),同理, 每次拓展 \([l,r]\) 到 \([l,r']\) 都会使得 \(e_{r'}\le e_r\),不断根据这个贪心拓展区间 \([l,r]\),当然最终可能拓展到一个区间 \([l^*,r^*]\) 使得任何 \(l'<l^*\) 都有 \(e_{l'}<e_{l^*}\) 且任何 \(r'>r*\) 都有 \(e_{r'}>e_{r^*}\),此时无论如何都无法继续拓展。
考虑此时进行时光倒流,我们用类似的方法从区间 \([1,n]\) 开始逆向收缩,容易证明逆向收缩的过程根据刚才的贪心一定可以收缩到 \([l^*,r^*]\)。
因此判断的时候后从 \([k,k]\) 和 \([1,n]\) 两端分别贪心地拓展和收缩到 \([l^*,r^*]\) 即可。
时间复杂度 \(\mathcal O(n\log V)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+1,INF=1e9;
int n,k,T,x[MAXN],e[MAXN];
inline bool check(int v) {
for(int i=1;i<=n;++i) e[i]=x[i]-2*T*v*i;
if(e[1]<e[n]) return false;
int lb=k,rb=k;
for(int i=k-1;i>=1;--i) if(e[i]>=e[lb]) lb=i;
for(int i=k+1;i<=n;++i) if(e[i]<=e[rb]) rb=i;
int l=k,r=k;
while(lb<l||r<rb) {
bool ok=false;
int lp=l;
while(lb<lp&&e[lp-1]>=e[r]) {
--lp; if(e[lp]>=e[l]) break;
}
if(lp<l&&e[lp]>=e[l]) l=lp,ok=true;
int rp=r;
while(rp<rb&&e[rp+1]<=e[l]) {
++rp; if(e[rp]<=e[r]) break;
}
if(rp>r&&e[rp]<=e[r]) r=rp,ok=true;
if(!ok) return false;
}
l=1,r=n;
while(l<lb||rb<r) {
bool ok=false;
int lp=l;
while(lp<lb&&e[lp+1]>=e[r]) {
++lp; if(e[lp]>=e[l]) break;
}
if(lp>l&&e[lp]>=e[l]) l=lp,ok=true;
int rp=r;
while(rb<rp&&e[rp-1]<=e[l]) {
--rp; if(e[rp]<=e[r]) break;
}
if(rp<r&&e[rp]<=e[r]) r=rp,ok=true;
if(!ok) return false;
}
return true;
}
signed main() {
scanf("%lld%lld%lld",&n,&k,&T);
for(int i=1;i<=n;++i) scanf("%lld",&x[i]);
int l=0,r=INF,res=INF;
while(l<=r) {
int mid=(l+r)>>1;
if(check(mid)) res=mid,r=mid-1;
else l=mid+1;
}
printf("%lld\n",res);
return 0;
}
D. Arranging Tickets
题目大意
在一个大小为 \(n\) 的环上,有 \(m\) 种区间,第 \(i\) 种为 \([l_i,r_i)\),有 \(c_i\) 个,对于每个区间可以选择覆盖环上的 \([l_i,r_i)\) 还是 \([r_i,l_i)\),最小化所有位置中被覆盖次数最多的位置的覆盖次数。
数据范围:\(n\le 2\times 10^5\)。
思路分析
算法一
显然第一步先二分答案 \(x\),我们把原问题放回到序列上,变成选择若干个 \(u_i=[l_i,r_i]\) 的区间变成 \([1,l_i)\cup(r_i,n]\) 然后最小化最大达覆盖次数。注意到如下的观察:
观察一:
存在一组最优解使得所有被反转的区间 \(\mathbf{u_i}\) 的并集不为空。
证明:
考虑存在两个反转的区间 \(u_i,u_j\) 使得 \([l_i,r_i]\cap[l_j,r_j]=\varnothing\),此时考虑同时取消 \(u_i\) 和 \(u_j\) 的反转。
此时 \(u_i,u_j\) 被覆盖的次数依然是一次,但是 \([1,n]-u_i-u_j\) 的覆盖次数从 \(2\) 次变成了 \(0\) 次,显然反转之后更优,因此若存在两个交集不为空的被反转区间,一定可以通过取消反转调整成一个更优的解。
设 \(\mathbf C\) 表示所有被选的区间的集合,记 \(a_i\) 表示每个区间原本被覆盖的次数,取某个 \(p\in \bigcap_{k\in\mathbf C} u_k\)。
先考虑 \(i\in[1,p)\),记 \(t_i\) 表示 \(i\) 被多少 \(k\in \mathbf C\) 的区间 \(u_k\) 包含,那么 \(i\) 实际被覆盖的次数 \(c_i\) 为 \(a_i-t_i+(t_p-t_i)=a_i-2t_i+t_p\),由于 \(c_i\le x\) 得到 \(t_i\ge \left\lceil\dfrac{a_i+t_p-x}2\right\rceil\),因此每个 \(t_i\) 有一定限制,显然从 \(1\) 到 \(p-1\) 依次遍历后贪心选右端点大的区间补齐 \(t_i\) 即可。最后暴力判断 \((p,n]\) 中的每个数覆盖次数是否合法即可。
每次判断的时候枚举 \(p,t_p\) 暴力检查即可。
时间复杂度 \(\mathcal O(n^2V\log n\log V)\),其中 \(V=\max_{i=1}^n\{a_i\}\)。
算法二
考虑优化 \(p\) 和 \(t_p\) 的枚举过程,注意到如下的观察:
观察二:
存在一组最优解使得 \(\mathbf{c_p\in\{\max\{c_i\},\max\{c_{i}\}-1\}}\)。
证明:
仅考虑 \(i\not\in\bigcap_{k\in\mathbf C} u_k\),若存在某个 \(c_i\ge c_p+2\),考虑取消某个区间的翻转,此时 \(c_p\) 至少变大 \(1\),\(\max\{c_i\}\) 要么变成 \(c_p\) 要么在原来的基础上减少,因此不断进行这个操作可以在保证解最优的情况下将 \(\max{c_i}-c_p\) 缩小到 \(0\) 或 \(1\)。
此时由于 \(c_p=a_p-t_p\in\{x,x-1\}\),因此我们得到 \(t_p\in\{a_p-x,a_p-x+1\}\),因此我们将 \(t_p\) 的枚举量从 \(\mathcal O(V)\) 减少到了 \(\mathcal O(1)\),检查答案的时候只需要枚举 \(p\) 即可。
时间复杂度 \(\mathcal O(n^2\log n\log V)\)。
算法三
继续挖掘 \(p\) 的性质,观察到:
观察三:
存在一组最优解使得 \(\mathbf{a_p=\max\{a_i\}}\)。
同样仅考虑 \(i\not\in\bigcap_{k\in\mathbf C} u_k\),此时 \(c_p=a_p-t_p,c_i=a_i+t_p-2t_i\),注意到 \(t_i<t_p\),因此 \(c_i=a_i+t_p-2t_i> a_i-t_p\)。
根据观察二可知 \(c_i\le c_p+1\),因此 \(a_p-t_p+1>a_i-t_p\),所以有 \(a_p\ge a_i\),原命题得证。
因此我们只需要取 \(a_p=\max\{a_i\}\) 的 \(p\) 即可,再用上述的方法确定 \(t_p\) 并求解即可。
时间复杂度 \(\mathcal O(n\log n\log V)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+1,INF=1e18;
int a[MAXN],b[MAXN],l[MAXN],r[MAXN],c[MAXN];
struct Info {
int r,cnt;
Info(int _r=0,int _c=0): r(_r),cnt(_c) {}
inline friend bool operator <(const Info &u,const Info &v) {
return u.r<v.r;
}
};
vector <Info> I[MAXN];
signed main() {
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;++i) {
scanf("%lld%lld%lld",&l[i],&r[i],&c[i]);
if(l[i]>r[i]) swap(l[i],r[i]);
a[l[i]]+=c[i],a[r[i]]-=c[i];
}
for(int i=1;i<=n;++i) a[i]+=a[i-1];
int pos=max_element(a+1,a+n+1)-a;
for(int i=1;i<=m;++i) {
if(l[i]<=pos&&pos<=r[i]) I[l[i]].push_back(Info(r[i],c[i]));
}
auto check=[&](int lim,int cnt) -> bool {
memset(b,0,sizeof(b));
priority_queue <Info> Q;
int cov=0;
for(int i=1;i<=n;++i) {
for(auto u:I[i]) Q.push(u);
while(a[i]+cnt-2*cov>lim) {
if(Q.empty()) return false;
auto u=Q.top(); Q.pop();
int f=min(u.cnt,(a[i]+cnt-lim+1)/2-cov);
// 2*cov>=a[i]+cnt-lim
cov+=f,b[u.r]+=f;
if(u.cnt>f) Q.push(Info(u.r,u.cnt-f));
}
}
for(int i=pos;i<=n;++i) {
b[i]+=b[i-1];
if(a[i]-cov+2*b[i]>lim) return false;
}
return true;
};
int l=0,r=a[pos],res=a[pos]+1;
while(l<=r) {
int mid=(l+r)>>1;
if(check(mid,a[pos]-mid)||check(mid,a[pos]-mid+1)) r=mid-1,res=mid;
else l=mid+1;
}
printf("%lld\n",res);
return 0;
}
E. Broken Device
题目大意
通信题:
Anna.cpp
:输入一个非负整数 \(x\),输出到一个 01 串 \(S\) 中并传递给Bruno.cpp
,已知 \(S\) 中第 \(p_1,p_2,\dots,p_k\) 位会在传递给Bruno.cpp
之前被赋值成 \(0\)。
Bruno.cpp
:输入被修改后的 01 串 \(S\),求出 \(x\) 的值。数据范围:\(x\le 10^{18},|S|=150,k\le 40\)。
思路分析
考虑以相邻两位保存信息,只要 \(i,i+1\) 有一个位被破坏那么设为 \(00\),剩下的 \(\{01,10,11\}\) 分别对应三进制下的数码 \(\{0,1,2\}\),以 \(3\) 进制的方式传递 \(x\)。
最坏情况下,我们剩下 \(35\) 组数码表示 \(x\),表出数的最大值为 \(3^{35}\approx 5\times 10^{16}\),考虑进一步优化:
首先可以通过随机化一个 \(1\sim|S|\) 的排列来得到相邻两位的分组,此时每组数码可用的概率 \(p\) 约为 \(\binom{|S|-2}{k}\div\binom{|S|}{k}\approx 0.536\),最终传递上界的期望为 \(3^{75p}\approx 1.57\times 10^{19}\),在随机化效果不好时依然有概率无法通过本题。
考虑一个优化:在只有第 \(i\) 位被破坏时,依然可以表示 \(01\),同理只有第 \(i+1\) 被破坏时依然可以表示 \(10\),因此这两种情况也能被利用,此时 \(p=\binom{|S|-2}{k}\div\binom{|S|}{k}+\frac 23\binom{|S|-2}{k-1}\div\binom{|S|}k\approx 0.6021\),标出上界的期望被进一步优化到 \(3.51\times 10^{21}\),足够通过本题。
代码呈现
Anna.cpp
:
#include<bits/stdc++.h>
#include "Annalib.h"
#define ll long long
using namespace std;
mt19937 RndEng(19260827);
void Anna(int N,ll X,int k,int P[]) {
vector <int> idx(N),mark(N,0);
for(int i=0;i<k;++i) mark[P[i]]=1;
iota(idx.begin(),idx.end(),0);
shuffle(idx.begin(),idx.end(),RndEng);
for(int i=0;i<N;i+=2) {
int u=idx[i],v=idx[i+1];
if(mark[u]&&mark[v]) {
Set(u,0),Set(v,0);
} else if(mark[u]) {
if(X%3==0) X/=3,Set(u,0),Set(v,1);
else Set(u,0),Set(v,0);
} else if(mark[v]) {
if(X%3==1) X/=3,Set(u,1),Set(v,0);
else Set(u,0),Set(v,0);
} else {
int d=X%3; X/=3;
if(d==0) Set(u,0),Set(v,1);
if(d==1) Set(u,1),Set(v,0);
if(d==2) Set(u,1),Set(v,1);
}
}
}
Bruno.cpp
:
#include<bits/stdc++.h>
#include"Brunolib.h"
#define ll long long
using namespace std;
mt19937 RndEng(19260827);
ll Bruno(int N,int A[]) {
vector <int> idx(N);
iota(idx.begin(),idx.end(),0);
shuffle(idx.begin(),idx.end(),RndEng);
ll X=0;
for(int i=N-2;i>=0;i-=2) {
int u=A[idx[i]],v=A[idx[i+1]];
if(!u&&!v) continue;
else if(!u) X=X*3+0;
else if(!v) X=X*3+1;
else X=X*3+2;
}
return X;
}
F. Railway Trip
题目大意
你有一个长度为 \(n\) 的序列 \(h_1,h_2,\dots ,h_n\),由该序列生成一个无项带权完全图,\(u,v\) 之间的边权(\(u<v\))定义为满足 \(u<i<v\) 且 \(h_i\ge\min(h_u,h_v)\) 的 \(i\) 的个数。
\(q\) 次询问 \(a\to b\) 的最短路。
数据范围:\(n,q\le 10^5\)。
思路分析
考虑刻画操作形态:假设 \(u\to v\) 的路径为 \(p_1,p_2,\dots ,p_k\),那么 \(p_i\) 一定是单峰的,也就是 \(p_1\le p_2\le\cdots\le p_u\ge p_{u+1}\ge\cdots\ge p_k\),证明可以贪心调整完成,进一步挖掘可以知道 \(p_i\to p_{i+1}\) 中间不存在其他大于 \(\min(h_{p_i},h_{p_{i+1}})\) 的位置,否则插入这个位置一定不劣。
综上,我们只需要处理每个数向左向右第一个大于自己的数,并且从 \(u,v\) 两边分别倍增上跳即可。
考虑如何倍增出 \(l(u,2^k)\) 和 \(r(u,2^k)\),一个经典的观察是到 \(l(u,2^k)\) 要么从 \(l(l(u,2^{k-1}),2^{k-1})\) 扩展要么从 \(l(r(u,2^{k-1}),2^{k-1})\) 扩展,求 \(r(u,2^k)\) 同理。
因此回答询问的时候从两侧分别倍增即可。
时间复杂度 \(\mathcal O((n+q)\log n)\)
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+1;
int a[MAXN],stk[MAXN],top,L[MAXN][20],R[MAXN][20];
signed main() {
int n,k,q;
scanf("%d%d%d",&n,&k,&q);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
top=0;
for(int i=1;i<=n;++i) {
while(top&&a[stk[top]]<a[i]) --top;
L[i][0]=top?stk[top]:i,stk[++top]=i;
}
top=0;
for(int i=n;i>=1;--i) {
while(top&&a[stk[top]]<a[i]) --top;
R[i][0]=top?stk[top]:i,stk[++top]=i;
}
for(int k=1;k<20;++k) {
for(int i=1;i<=n;++i) {
L[i][k]=min(L[L[i][k-1]][k-1],L[R[i][k-1]][k-1]);
R[i][k]=max(R[L[i][k-1]][k-1],R[R[i][k-1]][k-1]);
}
}
while(q--) {
int u,v;
scanf("%d%d",&u,&v);
if(u>v) swap(u,v);
int l=u,r=u,ans=0;
for(int k=19;k>=0;--k) {
if(max(R[l][k],R[r][k])<v) {
ans+=1<<k;
int a=l,b=r;
l=min(L[a][k],L[b][k]),r=max(R[a][k],R[b][k]);
}
}
u=r,l=v,r=v;
for(int k=19;k>=0;--k) {
if(min(L[l][k],L[r][k])>u) {
ans+=1<<k;
int a=l,b=r;
l=min(L[a][k],L[b][k]),r=max(R[a][k],R[b][k]);
}
}
printf("%d\n",ans);
}
return 0;
}
G. Long Distance Coach
题目大意
某长途巴士发车时刻为 \(0\),到达终点的时刻为 \(x\)。车上装有饮水机,乘客和司机可以在车上装水喝。出发前水箱是空的。途中有 \(n\) 个服务站,依次编号为 \(1\dots n\)。巴士到达服务站 \(i\) 的时间是 \(s_i\)。保证 \(s_1<s_2<\dots<s_n\) 严格递增,在服务站可以给饮水机加水,但是要钱,水价为每升 \(w\) 元。
本次巴士有 \(m\) 名乘客(不含司机),对于所有非负整数 \(k\),乘客 \(j\) 在时刻 \(kt+d_j\) 需要装 \(1\) 升水,在其他时刻不装水,司机在时刻 \(kt\) 需要装 \(1\) 升水,在其他时刻不装水。如果某一名乘客想装水时饮水机没水了,这名乘客会怒而下车,此时需要向这名乘客退 \(c_j\) 元,你需要保证司机每次都能装到水。
保证不会出现两人在同一时刻需要装水的情况,保证在服务站或是到达终点时,不存在司机或乘客需要喝水。求最小化对乘客的赔款和装水费用之和。
数据范围:\(n,m\le 2\times 10^5\)。
思路分析
注意到每次乘客下车都是 \(d_j\) 连续的一段,且不会越过 \(0\) 转移(司机不能下车)。
因此这是一个经典的数列分段问题,设 \(dp_i\) 表示前 \(i\) 个人的答案。
- \(i\) 一直不下车:\(dp_i\gets dp_{i-1}+w\left\lceil\dfrac{x-d_i}t\right\rceil\)。
- \((j,i]\) 下车:$dp_i\gets dp_j+C_i-C_j+(j-i)\times w\times \left\lfloor\dfrac {f_i}t\right\rfloor $,其中 \(C_i\) 是 \(c_i\) 的前缀和,\(f_i\) 表示 \(i\) 最早的合法下车时间 \(s_x\),注意这个下车时间要满足 \(d_i\le s_x\bmod t<d_{i+1}\),否则转移 \(i+1\) 一直不下车的时候可能会出错。
注意到第二种转移是典型的斜率优化,决策点为 \((j,dp_j-c_j)\),斜率阈值为 \(w\times \left\lfloor\dfrac {f_i}t\right\rfloor\),但这个东西没有单调性,只能二分单调栈解决。
时间复杂度 \(\mathcal O(n\log n+m\log m)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5,inf=1e18;
struct Passenger { int c,d; } a[MAXN];
int c[MAXN],d[MAXN],s[MAXN],id[MAXN];
int dp[MAXN],f[MAXN],q[MAXN];
signed main() {
int x,n,m,w,t;
scanf("%lld%lld%lld%lld%lld",&x,&n,&m,&w,&t);
for(int i=1;i<=n;++i) scanf("%lld",&s[i]);
s[++n]=x;
for(int i=1;i<=m;++i) scanf("%lld%lld",&a[i].d,&a[i].c);
sort(a+1,a+m+1,[&](Passenger u,Passenger v){ return u.d<v.d; });
for(int i=1;i<=m;++i) c[i]=c[i-1]+a[i].c,d[i]=a[i].d;
d[m+1]=t;
iota(id+1,id+n+1,1);
sort(id+1,id+n+1,[&](int u,int v) {
if(s[u]%t==s[v]%t) return s[u]<s[v];
return s[u]%t<s[v]%t;
});
fill(f+1,f+m+1,inf);
for(int i=1,p=1;i<=m;++i) {
while(p<=n&&s[id[p]]%t<=d[i]) ++p;
while(p<=n&&s[id[p]]%t<d[i+1]) f[i]=min(f[i],s[id[p]]/t*w),++p;
}
int hd=1,tl=1;
dp[0]=(x+t-1)/t*w;
for(int i=1;i<=m;++i) {
dp[i]=dp[i-1]+(x+t-d[i]-1)/t*w;
auto Y=[&](int j,int k) { return (dp[k]-c[k])-(dp[j]-c[j]); };
if(f[i]!=inf) {
int res=tl,l=hd,r=tl-1;
while(l<=r) {
int mid=(l+r)>>1;
if(Y(q[mid],q[mid+1])>f[i]*(q[mid+1]-q[mid])) res=mid,r=mid-1;
else l=mid+1;
}
int j=q[res];
dp[i]=min(dp[i],dp[j]+c[i]-c[j]+(i-j)*f[i]);
}
while(hd<tl&&Y(q[tl-1],q[tl])*(i-q[tl])>=Y(q[tl],i)*(q[tl]-q[tl-1])) --tl;
q[++tl]=i;
}
printf("%lld\n",dp[m]);
return 0;
}
H. Long Masion
题目大意
数轴上有 \(1\sim n\) 的房间,每个房间里有若干钥匙,房间 \(i\) 和房间 \(i+1\) 直接相连,但需要一种特定的钥匙开门。
经过房间可以获得该房间所有钥匙,钥匙可以重复使用。
\(q\) 次询问能否从 \(s\) 房间到 \(t\) 房间。
数据范围:\(n,q\le 5\times 10^5\)。
思路分析
注意到每个点 \(u\) 出发能到达的点都是一个区间 \([L_u,R_u]\)。
考虑记忆化搜索,每次判断 \(L_u\to L_u-1\) 和 \(R_u\to R_u+1\) 是否可行,可以通过处理每个门左侧和右侧最近的钥匙解决。
可以证明每个点只会开始拓展 \(\mathcal O(1)\) 次。
时间复杂度 \(\mathcal O(n\log n+q)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+1;
int n,q,L[MAXN],R[MAXN],c[MAXN],lk[MAXN],rk[MAXN];
//lk[i]: i->i-1, min{R[x]}
//rk[i]: i->i+1, max{L[x]}
bool vis[MAXN];
inline void expand(int x) {
vis[x]=true;
auto expandL=[&]() { return 1<L[x]&&lk[L[x]]<=R[x]; };
auto expandR=[&]() { return R[x]<n&&L[x]<=rk[R[x]]; };
while(expandL()||expandR()) {
if(expandL()) {
int u=L[x]-1;
if(!vis[u]) expand(u);
L[x]=min(L[x],L[u]),R[x]=max(R[x],R[u]);
}
if(expandR()) {
int u=R[x]+1;
if(!vis[u]) expand(u);
L[x]=min(L[x],L[u]),R[x]=max(R[x],R[u]);
}
}
}
vector <int> pos[MAXN];
signed main() {
scanf("%d",&n);
iota(L+1,L+n+1,1),iota(R+1,R+n+1,1);
for(int i=1;i<n;++i) scanf("%d",&c[i]);
for(int i=1;i<n;++i) pos[i].push_back(0);
for(int i=1;i<=n;++i) {
int k;
scanf("%d",&k);
while(k--) {
int id;
scanf("%d",&id);
pos[id].push_back(i);
}
}
for(int i=1;i<n;++i) pos[i].push_back(n+1);
for(int i=1;i<n;++i) rk[i]=*(--upper_bound(pos[c[i]].begin(),pos[c[i]].end(),i));
for(int i=n;i>1;--i) lk[i]=*lower_bound(pos[c[i-1]].begin(),pos[c[i-1]].end(),i);
for(int i=1;i<=n;++i) if(!vis[i]) expand(i);
scanf("%d",&q);
while(q--) {
int s,t;
scanf("%d%d",&s,&t);
puts(L[s]<=t&&t<=R[s]?"YES":"NO");
}
return 0;
}
I. Natural Park
题目大意
交互器内有一个 \(n\) 个的无向连通图 \(G=(V,E)\)。每次交互你可以询问一个 \(V'\subseteq V\) 和两个点 \(x,y\),交互器会告诉你 \(x,y\) 在 \(V'\) 的导出子图中是否连通。
试在 \(45000\) 次询问之内求出 \(G\)。
数据范围:\(n\le 1400,m\le 1500\),所有点度数 \(\le 7\)。
思路分析
先考虑怎么解决一条知道两个端点的链的情况。
考虑递归求解,每次确定某条链 \(u\to v\) 上编号最小的端点,二分一个 \(k\),加入所有 \(\le k\) 的点判断 \(u,v\) 是否联通,找到最小可能的 \(k\),这个 \(k\) 一定在链上,然后递归拆成 \(u\to k,k\to v\) 两条链,操作次数 \(n\log n\)。
然后考虑一般的情况,考虑增量构造,从 \(G=\{0\}\) 开始,每次加入一个 \(G\) 邻域中的点 \(u\) 并确定 \(u\to G\) 的边。
这一步也可以考虑二分,由于 \(G\) 在构造的时候一定是联通的,因此我们类比找链的方式,求 \(u\) 邻域中最小的点。
但这个东西很难 check,考虑一个简单的 check 想法,假如我们在 dfs 序上二分,找 dfs 序最小的点,那么 check 就很方便,每次只要二分一个 \(k\),判断加入 dfs 序 \(\le k\) 的所有点后,\(u\) 是否与根连通即可。
然后我们删掉和 \(u\) 相邻的这个点 \(t\),原树会被分成若干个连通块,对于每个连通块 \(B\),若 \(u\to B\) 有边就递归求解即可。
这里的操作次数分成两部分:对于二分部分,每条边只会被二分一次,操作次数 \(m\log n\),对于判断 \(u\to B\) 是否有边,显然删掉一个 \(t\) 后分出的连通块个数 \(\le \mathrm{deg}(t)\),因此这一部分的总次数不超过 \(7m\)。
然后考虑怎么找一个 \(G\) 邻域里的点,考虑用找链的方法,把所有 \(G\) 里的点看成一个点,然后任取一个 \(G\) 外的点 \(x\),用第一部分里的方法去暴力找一条 \(G\to x\) 的链,然后把链上的每个点依次加入即可,这一部分的总操作次数依然是 \(n\log n\) 点。
总操作次数 \((n+m)\log n+7m\le 42400\),可以通过此题。
代码呈现
#include<bits/stdc++.h>
#include "park.h"
using namespace std;
const int MAXN=1401;
int n;
struct Edge {
int u,v;
Edge(int _u=0,int _v=0): u(_u),v(_v) {}
};
inline int Query(int u,int v,vector <int> V) {
static int buf[MAXN];
fill(buf,buf+n,0);
for(int i:V) buf[i]=1;
buf[u]=buf[v]=1;
return Ask(min(u,v),max(u,v),buf);
}
inline void ReportEdge(int u,int v) { Answer(min(u,v),max(u,v)); }
inline vector<Edge> Solve() {
vector <int> V;
vector <Edge> E;
vector <int> inq(n,0);
vector <vector<int>> adj(n);
auto InsertNode=[&](int u) -> void {
V.push_back(u),inq[u]=1;
};
auto LinkEdge=[&](int u,int v) -> void {
assert(inq[u]&&inq[v]);
adj[u].push_back(v);
adj[v].push_back(u);
E.push_back({u,v});
};
InsertNode(0);
while((int)V.size()<n) {
vector <int> outq,vis(n,0);
for(int i=0;i<n;++i) if(!inq[i]) outq.push_back(i);
int nw=outq.front();
auto GetChain=[&](auto self,int l,int r) -> vector<int> {
vector <int> bas=(l==0)?V:vector<int>{};
if(Query(l,r,bas)) return {};
int ul=0,ur=n-1;
while(ul<ur) {
int mid=(ul+ur)>>1;
auto check=[&](int x) {
vector <int> qry=bas;
for(int i=0;i<=x;++i) qry.push_back(i);
return Query(l,r,qry);
};
if(check(mid)) ur=mid;
else ul=mid+1;
}
auto L=self(self,l,ur),R=self(self,ur,r);
vector <int> ans;
for(int i:L) ans.push_back(i);
ans.push_back(ur);
for(int i:R) ans.push_back(i);
return ans;
};
vector <int> chain=GetChain(GetChain,0,nw),ans;
chain.push_back(nw);
auto FindNeighbors=[&](int u) -> void {
auto MinLink=[&](auto self,vector <int> B) -> vector<int> {
vector <int> vis(n,1),dfn;
for(int i:B) vis[i]=0;
int rt=B.front();
auto dfs1=[&](auto self,int u) -> void {
dfn.push_back(u),vis[u]=1;
for(int v:adj[u]) if(!vis[v]) self(self,v);
};
dfs1(dfs1,rt);
int l=0,r=dfn.size()-1;
while(l<r) {
int mid=(l+r)>>1;
auto check=[&](int x) {
vector <int> subt;
for(int i=0;i<=x;++i) subt.push_back(dfn[i]);
return Query(rt,u,subt);
};
if(check(mid)) r=mid;
else l=mid+1;
}
vector <int> ans{dfn[r]};
fill(vis.begin(),vis.end(),1);
for(int i=0;i<(int)dfn.size();++i) if(i!=r) vis[dfn[i]]=0;
for(int v:V) {
if(vis[v]) continue;
vector <int> ver;
auto dfs2=[&](auto self,int u) -> void {
ver.push_back(u),vis[u]=1;
for(int v:adj[u]) if(!vis[v]) self(self,v);
};
dfs2(dfs2,v);
if(Query(ver.front(),u,ver)) {
vector <int> tmp=self(self,ver);
for(int x:tmp) ans.push_back(x);
}
}
return ans;
};
vector <int> Ne=MinLink(MinLink,V);
InsertNode(u);
for(int v:Ne) LinkEdge(u,v);
};
for(int i=0;i<(int)chain.size();++i) {
int u=chain[i];
FindNeighbors(u);
}
}
return E;
}
void Detect(int T,int N) {
n=N;
vector <Edge> G=Solve();
for(auto e:G) ReportEdge(e.u,e.v);
}
J. Abduction 2
题目大意
给一个 \(n\times m\) 的网格图,每行每列分别有权值。
你可以以如下方式在网格图上运动:
- 起始时任选方向。
- 当到达一个交点时:
- 若直走权值大于转弯权值,那么直走,如果前方是边界则结束运动。
- 否则选一种方向转向。
\(q\) 次询问从 \((s,t)\) 出发的最长运动距离。
数据范围:\(n,m\le 5\times 10^4,q\le 100\)。
思路分析
直接记搜 \(dp(i,j,d)\) 表示到达第 \((i,j)\) 行接下来方向为 \(j\) 的最长路,用 ST 表快速求下一个转向的位置。
对于每个询问,把拓展到的点围成一个矩形,显然在矩形内部的路径一定会走到矩形上的并转向。
而矩形内的点只要 \(\mathcal O(1)\) 步走到矩形边界上,因此最终态就是矩形面积和为 \(\mathcal O(n^2)\) 级别。
显然每个矩形进行一次搜索就会使得长或宽增加至少 \(1\),并且一定是交替增加长和宽的。
因此每个矩形拓展 \(\dfrac n{\sqrt q}\) 步后,矩形周长为 \(\mathcal O\left(\dfrac n{\sqrt q}\right)\) 级别,这一部分总拓展次数为 \(\mathcal O(n\sqrt q)\),接下来每一次拓展都会使得矩形面积和增加至少 \(\mathcal O\left(\dfrac n{\sqrt q}\right)\),拓展次数为 \(\mathcal O(n\sqrt q)\) 级别。
时间复杂度 \(\mathcal O(n\sqrt q\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e4+1;
struct RMQ {
int f[MAXN][18];
inline int bit(int x) { return 1<<x; }
inline void build(int *a,int n) {
for(int i=1;i<=n;++i) f[i][0]=a[i];
for(int k=1;k<18;++k) {
for(int i=1;i+bit(k-1)<=n;++i) {
f[i][k]=max(f[i][k-1],f[i+bit(k-1)][k-1]);
}
}
}
inline int query(int l,int r) {
int k=__lg(r-l+1);
return max(f[l][k],f[r-bit(k)+1][k]);
}
} R,C;
int n,m,q,a[MAXN],b[MAXN];
map <tuple<int,int,int>,int> dp;
const int dx[]={-1,0,1,0},dy[]={0,-1,0,1};
inline int dfs(int x,int y,int d) {
//d: {U,L,D,R}
auto T=make_tuple(x,y,d);
if(dp.count(T)) return dp[T];
auto valid=[&](int i,int j) { return 1<=i&&i<=n&&1<=j&&j<=m; };
if(!valid(x+dx[d],y+dy[d])) return dp[T]=0;
int l=1,r=max(n,m),res=0;
auto check=[&](int k) -> bool {
if(!valid(x+k*dx[d],y+k*dy[d])) return false;
if(d==0) return R.query(x-k,x-1)<=b[y];
if(d==1) return C.query(y-k,y-1)<=a[x];
if(d==2) return R.query(x+1,x+k)<=b[y];
if(d==3) return C.query(y+1,y+k)<=a[x];
return 0;
};
while(l<=r) {
int mid=(l+r)>>1;
if(check(mid)) res=mid,l=mid+1;
else r=mid-1;
}
int nx=x+(res+1)*dx[d],ny=y+(res+1)*dy[d];
return dp[T]=valid(nx,ny)?max(dfs(nx,ny,d^1),dfs(nx,ny,d^3))+res+1:res;
}
signed main() {
scanf("%lld%lld%lld",&n,&m,&q);
for(int i=1;i<=n;++i) scanf("%lld",&a[i]);
for(int i=1;i<=m;++i) scanf("%lld",&b[i]);
R.build(a,n),C.build(b,m);
while(q--) {
int x,y;
scanf("%lld%lld",&x,&y);
printf("%lld\n",max(max(dfs(x,y,0),dfs(x,y,2)),max(dfs(x,y,1),dfs(x,y,3))));
}
return 0;
}
K. City
题目大意
通信题,实现两个程序:
encoder.cpp
:读入一棵 \(n\) 个点的深度不超过 \(19\) 的树,为每个点 \(u\) 分配一个 \([0,2^{28}-1)\) 之内的编号 \(T_u\)。device.cpp
:\(q\) 次询问,读入 \(T_x,T_y\),回答 \(x\) 和 \(y\) 在树上的祖先后代关系。数据范围:\(n\le 2.5\times 10^5\)。
思路分析
考虑 dfs 序,维护 \(dfn_u\) 和 \(siz_u\),但每个信息都要占 \(19\) 位,我们至多只能维护其中一个。
显然 \(dfn\) 更重要一些,因此考虑把 \(siz\) 信息压缩到 \(9\) 位之内。
考虑用一个序列去拟合 \(siz\),即找到一个序列 \(f_0\sim f_{511}\),然后找到第一个 \(f_i\ge siz_u\) 的 \(i\),并且给 \(u\) 加入 \(f_i-siz_u\) 个虚拟儿子,这样就只需要传递 \(i\)。
这个序列需要满足 \(f_{511}\ge 2^{19}\) 且个 \(\sum f_i-siz_u\le 2^{19}-n\)。
考虑构造等比数列,令 $f_i=\lfloor (1+q)^i\rfloor $,其中 \(q\) 是某个很小的实数。那么每个点增加的虚儿子不超过 \(q\times siz_u\) 级别,又因为每个点深度不超过 \(19\),因此 \(\sum siz_u\le 19n\),因此树的总点数不超过 \((1+19q)n\)。
事实上不需要严格令 \(f_i=\lfloor (1+q)^i\rfloor\),可以令 \(f_i=\max(1,f_{i-1}\times q))+f_{i-1}\),这样避免了前期增长速度过慢的问题。
可以手动二分一下找到一个合法的 \(q\),实际上取 \(q\in [0,22,0.45]\) 中的实数都符合条件。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
encoder.cpp
:
#include<bits/stdc++.h>
#define double long double
#include "Encoder.h"
using namespace std;
const int MAXN=2.5e5+1,C=512;
const double Q=1.023;
vector <int> G[MAXN];
int dfn[MAXN],siz[MAXN],dcnt=0,pw_e[C];
inline void dfs(int p,int fa) {
dfn[p]=++dcnt,siz[p]=1;
for(int v:G[p]) if(v!=fa) dfs(v,p),siz[p]+=siz[v];
int k=lower_bound(pw_e,pw_e+C,siz[p])-pw_e;
siz[p]=pw_e[k],dcnt=dfn[p]+siz[p]-1;
Code(p,k+1ll*C*dfn[p]);
}
void Encode(int N,int A[],int B[]) {
for(int i=0;i<N-1;++i) {
G[A[i]].push_back(B[i]);
G[B[i]].push_back(A[i]);
}
pw_e[0]=1;
for(int i=1;i<C;++i) pw_e[i]=max(pw_e[i-1]+1,(int)(pw_e[i-1]*Q));
dfs(0,0);
}
device.cpp
:
#include<bits/stdc++.h>
#define double long double
#define ll long long
#include "Device.h"
using namespace std;
const int MAXN=2.5e5+1,C=512;
const double Q=1.023;
int pw_d[C];
void InitDevice() {
pw_d[0]=1;
for(int i=1;i<C;++i) pw_d[i]=max(pw_d[i-1]+1,(int)(pw_d[i-1]*Q));
}
int Answer(ll S,ll T) {
int d1=S/C,s1=pw_d[S%C];
int d2=T/C,s2=pw_d[T%C];
if(d2<=d1&&d1<d2+s2) return 0;
if(d1<=d2&&d2<d1+s1) return 1;
return 2;
}
L. Dragon 2
题目大意
二维平面上有 \(n\) 个点,和一条线段 \(S\),每个点有 \(1\sim m\) 中的一种颜色。
\(q\) 次询问 \(x,y\),表示所有颜色为 \(x\) 的点向颜色为 \(y\) 的点连一条射线,有多少条射线与 \(S\) 相交。
数据范围:\(n\le 30000,q\le 10^5\)。
思路分析
首先考虑如何刻画连线相交,先旋转坐标系使得 \(S\) 连接 \((0,0),(1,0)\)。
然后维护每个点 \(i\) 与 \(S\) 构成的三角形的两个底角 \(\theta_i,\varphi_i\):
- 如果 \(j\) 和 \(i\) 在 X 轴的同侧,那么 \(i\to j\) 的射线与 \(S\) 相交当且仅当 \(\theta_j\le\theta_i\) 且 \(\varphi_j\le \varphi_i\)。
- 如果 \(j\) 和 \(i\) 在 X 轴的异侧,那么 \(i\to j\) 的射线与 \(S\) 相交当且仅当 \(\theta_j\le \pi-\theta_i\) 且 \(\varphi_j\le \pi-\varphi_i\)。
容易发现同一个 \(i\) 对答案的贡献是一个二维数点问题,维护 X 轴两侧两侧 \((\theta_i,\varphi_i),(\pi-\theta_i,\pi-\varphi_j)\) 分别对应的主席树即可。
然后考虑怎么计算答案,显然的优化是优先枚举大小较小的一种颜色,根据经典结论,答案是根号级别的:
- 如果两个集合大小都 \(\ge B\),那么这样的集合只有 \(\mathcal O\left(\dfrac nB\right)\) 个,因此每个集合最多被算 \(\mathcal O\left(\dfrac nB\right)\) 次,总计算次数为 \(O\left(\dfrac {n^2}B\right)\)。
- 如果有一个集合大小 \(<B\),那么这一部分计算次数为 \(\mathcal O(qB)\)。
均值不等式得到平均后的计算次数为 \(\mathcal O(n\sqrt q)\)。
计算几何处理时注意精度。
时间复杂度 \(\mathcal O(n\sqrt q\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
#define double long double
using namespace std;
const int MAXN=30001;
const double pi=acosl(-1),eps=1e-20;
vector <array<double,2>> dat[MAXN]; //(x,y) = (p[0],p[1])
vector <array<double,2>> orgP[MAXN][2]; //0: I & II, 1: III & IV
vector <array<double,4>> angs[MAXN][2]; //(ang_x,ang_y,180-ang_x,180-ang_y)
vector <array<int,4>> rnk[MAXN][2]; //rank of {ang[i][j]}
class SegmentTree {
private:
struct Node {
int ls,rs,sum;
};
vector <Node> tree;
int siz;
inline int Append(int u,int l,int r,int pre) {
int now=++siz;
if(l==r) { tree[now].sum=tree[pre].sum+1; return now; }
int mid=(l+r)>>1;
if(u<=mid) {
tree[now].ls=Append(u,l,mid,tree[pre].ls);
tree[now].rs=tree[pre].rs;
} else {
tree[now].ls=tree[pre].ls;
tree[now].rs=Append(u,mid+1,r,tree[pre].rs);
}
tree[now].sum=tree[tree[now].ls].sum+tree[tree[now].rs].sum;
return now;
}
inline int Count(int ul,int ur,int l,int r,int pos) {
if(!pos||(ul<=l&&r<=ur)) return tree[pos].sum;
int mid=(l+r)>>1,ans=0;
if(ul<=mid) ans+=Count(ul,ur,l,mid,tree[pos].ls);
if(mid<ur) ans+=Count(ul,ur,mid+1,r,tree[pos].rs);
return ans;
}
public:
vector <int> vals,root;
int n;
inline void Build(vector <array<int,2>> &V) {
sort(V.begin(),V.end());
tree.resize(20*((int)V.size())+5);
vals.push_back(0);
root.push_back(0);
for(int i=0;i<(int)V.size();++i) {
int nxt=Append(V[i][1],1,n,root[i]);
root.push_back(nxt);
vals.push_back(V[i][0]);
}
}
inline int Query(int x,int y,int opr) {
if(opr==0) {
int r=upper_bound(vals.begin(),vals.end(),x)-vals.begin()-1;
return Count(1,y,1,n,root[r]);
}
if(opr==1) {
int r=lower_bound(vals.begin(),vals.end(),x)-vals.begin()-1;
return Count(y,n,1,n,root.back())-Count(y,n,1,n,root[r]);
}
return 0;
}
} TR[MAXN][2][2];
signed main() {
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;++i) {
double a,b; int c;
scanf("%Lf%Lf%lld",&a,&b,&c);
dat[c].push_back({a,b});
}
double d1,e1,d2,e2;
scanf("%Lf%Lf%Lf%Lf",&d1,&e1,&d2,&e2);
if(d2<d1) swap(d1,d2),swap(e1,e2);
double Oang=-atan2l((double)(e2-e1),(double)(d2-d1));
double sin_ang=sinl(Oang),cos_ang=cosl(Oang);
const double DX=d1,DY=e1;
auto Transfer=[&](double &x,double &y) -> void {
double tx=x-DX,ty=y-DY;
x=tx*cos_ang-ty*sin_ang,y=tx*sin_ang+ty*cos_ang;
};
Transfer(d1,e1),Transfer(d2,e2);
for(int i=1;i<=m;++i) {
for(auto p:dat[i]) {
Transfer(p[0],p[1]);
int b=(p[1]>0)?0:1;
orgP[i][b].push_back({p[0],p[1]});
}
}
vector <double> X,Y;
for(int i=1;i<=m;++i) for(int j:{0,1}) {
for(auto p:orgP[i][j]) {
double ang1=atan2l((double)abs(p[1]),(double)p[0]);
double ang2=atan2l((double)abs(p[1]),(double)(d2-p[0]));
angs[i][j].push_back({ang1,ang2,pi-ang1,pi-ang2});
X.push_back(ang1),X.push_back(pi-ang1);
Y.push_back(ang2),Y.push_back(pi-ang2);
}
}
auto RealEqual=[&](double u,double v) { return fabs(u-v)<=eps; };
sort(X.begin(),X.end());
X.erase(unique(X.begin(),X.end(),RealEqual),X.end());
sort(Y.begin(),Y.end());
Y.erase(unique(Y.begin(),Y.end(),RealEqual),Y.end());
for(int i=1;i<=m;++i) for(int j:{0,1}) {
for(auto p:angs[i][j]) {
auto Idx=[&](const vector <double> &vals,double RealValue) {
return lower_bound(vals.begin(),vals.end(),RealValue)-vals.begin()+1;
};
rnk[i][j].push_back({Idx(X,p[0]),Idx(Y,p[1]),Idx(X,p[2]),Idx(Y,p[3])});
}
}
for(int i=1;i<=m;++i) for(int j:{0,1}) {
vector <array<int,2>> vals[2];
for(auto u:rnk[i][j]) {
vals[0].push_back({u[0],u[1]});
vals[1].push_back({u[2],u[3]});
}
for(int k:{0,1}) {
TR[i][j][k].n=Y.size();
TR[i][j][k].Build(vals[k]);
}
}
int q;
scanf("%lld",&q);
while(q--) {
int u,v,ans=0;
scanf("%lld%lld",&u,&v);
if(dat[u].size()<dat[v].size()) {
for(int s:{0,1}) for(auto w:rnk[u][s]) {
ans+=TR[v][s][0].Query(w[0],w[1],0);
ans+=TR[v][s^1][0].Query(w[2],w[3],0);
}
} else {
for(int s:{0,1}) for(auto w:rnk[v][s]) {
ans+=TR[u][s][0].Query(w[0],w[1],1);
ans+=TR[u][s^1][1].Query(w[0],w[1],1);
}
}
printf("%lld\n",ans);
}
return 0;
}