JOISC2017 题解
A. Cultivation
题目大意
在一个
的网格上有 个格子是黑色的。 每次操作可以把所有黑色格子向上下左右的某个方向扩展一格,即把所有黑色格子的左侧染成黑色(其他三个方向同理)。
求至少需要几次操作染黑整个网格。
数据范围:
。
思路分析
算法一
爆搜,时间复杂度
算法二
注意到每个节点在经过若干次操作后一定会扩展出一个矩形,可以证明无论如何调换操作顺序,最终的矩形形状不变,因此我们得到了一个重要结论:最终扩展出的图形只和上下左右操作的次数有关系,而和其相对顺序无关。
因此枚举四种操作分别进行的次数
算法三
先考虑
- 在最左侧黑格外:
,为了保证这些格子被染色应该有 。 - 在两个黑格之间:
,为了保证这些格子被染色应该有 。 - 在最右侧黑格外:
,为了保证这些格子被染色应该有 。
综上,
因此我们可以在
回到原问题,我们同样可以枚举
算法四
观察到对于每一行,染到这一行的点按
进一步观察发现对于每一对
因此我们把所有
注意到如果提前把所有
算法五
考虑
因此我们枚举
时间复杂度
算法六
观察到很多
可以证明此时要么
当然我们要计算出
时间复杂度
代码呈现
#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
题目大意
有
个物品和两个栈(后进先出),每个物品可能进入其中某个栈,现已知 个物品的进出栈顺序,求有多少种安排物品入栈出栈的方式满足顺序要求。 数据范围:
。
思路分析
假如我们把每个栈的入栈出栈时间看成一个线段的话,那么同一个栈中的线段要么不相交要么包含,因此不能在同一个栈中的物品可以写成若干组关于
假如我们把不能在同一个栈中的物品相连,那么这就是一个统计 2-SAT 解数的问题,若原图是二分图则答案为
注意到题目中的二维偏序限制关系可以用主席树优化建图,然后用扩域并查集统计答案。
下面简单讲一下如何实现解题目中的 2-SAT:
- 首先建出主席树,把每个线段挂到对应的主席树的叶子节点上,并把这两个节点设为同色。
- 对于每个二维偏序限制,从对应线段节点连到主席树某个区间节点上,并把这两个节点设为异色。
- 把所有被连边的主席树区间节点取出,将这些节点的子树全部设为同色。
前两步并查集就可以直接做,而第三步需要离线出所有节点再 BFS 一遍以保证复杂度。
时间复杂度
代码呈现
#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
题目大意
数轴上有
个人,其位置分别为 ,开始时, 号手中的烟花处于刚被点亮状态,若一个烟花不被点亮的人 和一个烟花被点亮的的人到 达同一位置后, 手中的烟花会被点亮 每个点亮的烟花可以持续燃烧
秒,每个人可以以任意非负整数的速度移动,求所有人的最大速度至少是多少才能使每个人至少被点亮一次 数据范围:
。
思路分析
显然本题答案具有可二分性,二分一个最大速度
考虑刻画点燃烟花棒的过程,显然某个时刻场上有两个点燃的烟花一定不优于把后点燃的一个留住,跟着先点燃的那个人跑,直到先点燃的那个人燃烧结束时再传火,可以证明这样一定更优。
一个显然的观察是:对于任意时刻,点燃过烟花的人一定是一个包含
令
因此我们需要从
因此每次我们拓展
考虑此时进行时光倒流,我们用类似的方法从区间
因此判断的时候后从
时间复杂度
代码呈现
#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
题目大意
在一个大小为
的环上,有 种区间,第 种为 ,有 个,对于每个区间可以选择覆盖环上的 还是 ,最小化所有位置中被覆盖次数最多的位置的覆盖次数。 数据范围:
。
思路分析
算法一
显然第一步先二分答案
观察一:
存在一组最优解使得所有被反转的区间
的并集不为空。 证明:
考虑存在两个反转的区间
使得 ,此时考虑同时取消 和 的反转。 此时
被覆盖的次数依然是一次,但是 的覆盖次数从 次变成了 次,显然反转之后更优,因此若存在两个交集不为空的被反转区间,一定可以通过取消反转调整成一个更优的解。
设
先考虑
每次判断的时候枚举
时间复杂度
算法二
考虑优化
观察二:
存在一组最优解使得
。 证明:
仅考虑
,若存在某个 ,考虑取消某个区间的翻转,此时 至少变大 , 要么变成 要么在原来的基础上减少,因此不断进行这个操作可以在保证解最优的情况下将 缩小到 或 。
此时由于
时间复杂度
算法三
继续挖掘
观察三:
存在一组最优解使得
。 同样仅考虑
,此时 ,注意到 ,因此 。 根据观察二可知
,因此 ,所以有 ,原命题得证。
因此我们只需要取
时间复杂度
代码呈现
#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
:输入一个非负整数,输出到一个 01 串 中并传递给 Bruno.cpp
,已知中第 位会在传递给 Bruno.cpp
之前被赋值成。
Bruno.cpp
:输入被修改后的 01 串,求出 的值。 数据范围:
。
思路分析
考虑以相邻两位保存信息,只要
最坏情况下,我们剩下
首先可以通过随机化一个
考虑一个优化:在只有第
代码呈现
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
题目大意
你有一个长度为
的序列 ,由该序列生成一个无项带权完全图, 之间的边权( )定义为满足 且 的 的个数。
次询问 的最短路。 数据范围:
。
思路分析
考虑刻画操作形态:假设
综上,我们只需要处理每个数向左向右第一个大于自己的数,并且从
考虑如何倍增出
因此回答询问的时候从两侧分别倍增即可。
时间复杂度
代码呈现
#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
题目大意
某长途巴士发车时刻为
,到达终点的时刻为 。车上装有饮水机,乘客和司机可以在车上装水喝。出发前水箱是空的。途中有 个服务站,依次编号为 。巴士到达服务站 的时间是 。保证 严格递增,在服务站可以给饮水机加水,但是要钱,水价为每升 元。 本次巴士有
名乘客(不含司机),对于所有非负整数 ,乘客 在时刻 需要装 升水,在其他时刻不装水,司机在时刻 需要装 升水,在其他时刻不装水。如果某一名乘客想装水时饮水机没水了,这名乘客会怒而下车,此时需要向这名乘客退 元,你需要保证司机每次都能装到水。 保证不会出现两人在同一时刻需要装水的情况,保证在服务站或是到达终点时,不存在司机或乘客需要喝水。求最小化对乘客的赔款和装水费用之和。
数据范围:
。
思路分析
注意到每次乘客下车都是
因此这是一个经典的数列分段问题,设
一直不下车: 。 下车: ,其中 是 的前缀和, 表示 最早的合法下车时间 ,注意这个下车时间要满足 ,否则转移 一直不下车的时候可能会出错。
注意到第二种转移是典型的斜率优化,决策点为
时间复杂度
代码呈现
#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
题目大意
数轴上有
的房间,每个房间里有若干钥匙,房间 和房间 直接相连,但需要一种特定的钥匙开门。 经过房间可以获得该房间所有钥匙,钥匙可以重复使用。
次询问能否从 房间到 房间。 数据范围:
。
思路分析
注意到每个点
考虑记忆化搜索,每次判断
可以证明每个点只会开始拓展
时间复杂度
代码呈现
#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
题目大意
交互器内有一个
个的无向连通图 。每次交互你可以询问一个 和两个点 ,交互器会告诉你 在 的导出子图中是否连通。 试在
次询问之内求出 。 数据范围:
,所有点度数 。
思路分析
先考虑怎么解决一条知道两个端点的链的情况。
考虑递归求解,每次确定某条链
然后考虑一般的情况,考虑增量构造,从
这一步也可以考虑二分,由于
但这个东西很难 check,考虑一个简单的 check 想法,假如我们在 dfs 序上二分,找 dfs 序最小的点,那么 check 就很方便,每次只要二分一个
然后我们删掉和
这里的操作次数分成两部分:对于二分部分,每条边只会被二分一次,操作次数
然后考虑怎么找一个
总操作次数
代码呈现
#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
题目大意
给一个
的网格图,每行每列分别有权值。 你可以以如下方式在网格图上运动:
- 起始时任选方向。
- 当到达一个交点时:
- 若直走权值大于转弯权值,那么直走,如果前方是边界则结束运动。
- 否则选一种方向转向。
次询问从 出发的最长运动距离。 数据范围:
。
思路分析
直接记搜
对于每个询问,把拓展到的点围成一个矩形,显然在矩形内部的路径一定会走到矩形上的并转向。
而矩形内的点只要
显然每个矩形进行一次搜索就会使得长或宽增加至少
因此每个矩形拓展
时间复杂度
代码呈现
#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
:读入一棵个点的深度不超过 的树,为每个点 分配一个 之内的编号 。 device.cpp
:次询问,读入 ,回答 和 在树上的祖先后代关系。 数据范围:
。
思路分析
考虑 dfs 序,维护
显然
考虑用一个序列去拟合
这个序列需要满足
考虑构造等比数列,令
事实上不需要严格令
可以手动二分一下找到一个合法的
时间复杂度
代码呈现
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
题目大意
二维平面上有
个点,和一条线段 ,每个点有 中的一种颜色。
次询问 ,表示所有颜色为 的点向颜色为 的点连一条射线,有多少条射线与 相交。 数据范围:
。
思路分析
首先考虑如何刻画连线相交,先旋转坐标系使得
然后维护每个点
- 如果
和 在 X 轴的同侧,那么 的射线与 相交当且仅当 且 。 - 如果
和 在 X 轴的异侧,那么 的射线与 相交当且仅当 且 。
容易发现同一个
然后考虑怎么计算答案,显然的优化是优先枚举大小较小的一种颜色,根据经典结论,答案是根号级别的:
- 如果两个集合大小都
,那么这样的集合只有 个,因此每个集合最多被算 次,总计算次数为 。 - 如果有一个集合大小
,那么这一部分计算次数为 。
均值不等式得到平均后的计算次数为
计算几何处理时注意精度。
时间复杂度
代码呈现
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】