JOISC写题记录 2017

「JOISC 2017 Day 1」港口设施

思路一(自己):

  同一个栈里的线段要么互不相交,要么就是包含关系.

  假如我们将所有相交的两条线段连一条边,表示它们不能在同一个栈里.最后连成的每个联通块如果不是一个二分图的话,就是无解的.如果是二分图,那么我们只要数一数联通块的个数\(cnt\),最后的答案就是\(2^{cnt}\).

  但是如果暴力连边的话,显然会\(T\)掉,因为边数最多可能有\(n^2\)条.一开始我想是不是环的点数只有可能是\(3\),于是敲了一个线段树,给每条线段连边的时候,用线段是查询一下\(l[i]+1,r[i]-1\)范围内的线段,在线段树的每个节点上开一个优先队列,每次将最大的弹出,用并查集维护一下是否出现了环.最后再将这些弹出的塞回去.

  但是这样是错的,比如下面这个样例就是\(4\)个点的环.

4
1 4
2 7
3 6
5 8

  从暴力重新思考,这时我发现弹出的那些线段没有必要都塞回来,因为如果还是有解的话,那么这些弹出来的线段明显都合并过了,那么它们就是在一个并查集里,所以我们只要将最大的那个塞回去即可.
  时间复杂度\(O(n\log n^2)\).

思路二:

  这里有一个更加优秀的写法,复杂度为\(O(n)\).(但是不知道为什么网上说是\(O(n\log n)\))
  上面的线段树不是很必要可以去掉,我们可以维护一个时间轴.当我们处理一个右端点的时候,我们需要的线段是左端点在\(l[i],r[i]\)范围内的,而且右端点超过\(r[i]\)的线段.

  那么相当于我们顺着这个时间轴的时候,每次遇到一个左端点时,将其加入一个数组里.遇到一个右端点的时候将其删除.我们可以从当前的右端点相匹配的左端点出发,一直往右走,路上遇到的还未删除的线段就加边.

  像上面的一样思考如何简化边数,发现如果存在一个点向\([a,b]\)连边,那么\([a,b]\)在最后的二分图上一定是同色的,那么当我们再次遇到\([a,b]\)这个区间,显然我们只需要连一条边即可,我们可以将\([a,b]\)的\(nxt\)都设成\(b\),这样我们只会踩一个点,就可以走到\(nxt+1\).这样边数就降到了\(O(n)\)级别.

  现在还剩下一个问题,我们不会直接遍历这个区间,那么删除操作该如何维护?其实我们可以用并查集.删除一个点相当于这个点的\(fa\)指向下一个点.最后\(dfs\)遍历一遍,判断是不是二分图即可.

代码一:
#include<bits/stdc++.h>
using namespace std;
#define ls p<<1
#define rs p<<1|1
const int M=2e6+5,P=1e9+7;
struct info {
	int l,r;
	bool operator <(const info &_)const {
		return l<_.l;	
	}
}A[M];
int n;
int fa[M],top;
int find(int g) {return fa[g]==g?fa[g]:fa[g]=find(fa[g]);}
struct node {
	int s,idx;
	bool operator <(const node &_)const {
		return s<_.s;	
	}
}stk[M];
bool Merge(int x,int y) {
	int sx=find(x),sy=find(y);
	int sxx=find(x+n),syy=find(y+n);
	fa[syy]=sx;fa[sxx]=sy;
	return find(x)==find(y);
}
int col[M];
bool flag[M];
struct Seg {
	priority_queue<node>S[M<<2];
	void Ins(int p,int l,int r,int pos,int val,int idx) {
		S[p].push((node)<%val,idx%>);
		if(l==r)return;
		int mid=l+r>>1;
		if(pos<=mid)Ins(ls,l,mid,pos,val,idx);
		else Ins(rs,mid+1,r,pos,val,idx);
	}
	void Get(int p,int l,int r,int lx,int rx,int val,int idx) {
		if(l==lx&&r==rx) {
			while(!S[p].empty()) {
				node A=S[p].top();
				if(A.s<val)break;
				S[p].pop();stk[++top]=A;
				if(Merge(A.idx,idx))printf("0"),exit(0);
			}
			if(top)S[p].push(stk[1]);
			top=0;
			return;
		}
		int mid=l+r>>1;
		if(rx<=mid)Get(ls,l,mid,lx,rx,val,idx);
		else if(lx>mid)Get(rs,mid+1,r,lx,rx,val,idx);
		else Get(ls,l,mid,lx,mid,val,idx),Get(rs,mid+1,r,mid+1,rx,val,idx);
	}
}Tree;
int main() {
	n=rd();
	for(int i=1; i<=n+n; ++i)fa[i]=i;
	for(int i=1; i<=n; ++i)A[i].l=rd(),A[i].r=rd();
	sort(A+1,A+n+1);
	for(int i=1; i<=n; ++i)Tree.Ins(1,1,n+n,A[i].l,A[i].r,i);
	for(int i=1; i<=n; ++i)if(A[i].l+1<A[i].r)Tree.Get(1,1,n+n,A[i].l+1,A[i].r-1,A[i].r,i);
	int res=0,ans=1;
	for(int i=1; i<=n+n; ++i)if(find(i)==i)res++;
	for(int i=1; i<=res/2; ++i)ans=(ans+ans)%P;
	printf("%d",ans);
	return 0;
}
代码二:
#include<bits/stdc++.h>
using namespace std;
const int M=2e6+5,P=1e9+7;
int n,Tot;
int l[M],r[M];
int id[M];
int hd[M],to[M],nx[M];
void add(int u,int v) {
	nx[++Tot]=hd[u];
	to[Tot]=v;hd[u]=Tot;
}
int fa[M];
int find(int g) {return fa[g]==g?g:fa[g]=find(fa[g]);}
int stk[M],top;
int num[M],nxt[M];
int col[M];
void dfs(int now) {
	for(int i=hd[now]; i; i=nx[i]) {
		int To=to[i];
		if(~col[To]) {
			if(col[To]==col[now])puts("0"),exit(0);	
		}else col[To]=col[now]^1,dfs(To);
	}
}
int main() {
	n=rd();memset(col,-1,sizeof col);
	for(int i=1; i<=n; ++i) {
		l[i]=rd();r[i]=rd();
		id[l[i]]=i;id[r[i]]=i;	
	}
	for(int i=1; i<=n; ++i)nxt[i]=fa[i]=i;nxt[n+1]=fa[n+1]=n+1;
	for(int i=1; i<=n+n; ++i) {
		int idx=id[i];
		if(l[idx]==i)stk[++top]=idx,num[idx]=top;
		else {
			for(int j=fa[num[idx]]=find(num[idx]+1),k; j<=top; j=k) {
				add(idx,stk[j]);add(stk[j],idx);
				k=find(nxt[j]+1);
				nxt[j]=top;
			}
		}
	}
	int res=1;
	for(int i=1; i<=n; ++i) {
		if(~col[i])continue;
		col[i]=0,dfs(i);res=res+res;
		(res>=P)&&(res-=P);	
	}printf("%d",res);
	return 0;
}

「JOISC 2017 Day 1」烟花棒

思路:
  • 在一个时间点,最多有一个人会点燃烟花棒.

证明: 若燃着的\(A\)碰到了\(B\),那么可以知道若\(B\)先跟着\(A\)走等到\(A\)没掉了再点燃,跟\(A\)先点燃\(B\)再走,能走的距离其实是相等的.

  所以我们碰到一个人相到于点燃时间增加\(T\).现在我们位于\(k\)点.

  我们可以知道如果我往左走,那么右边的点一定是往左走,左边的点一定往右走.那么我们与右边的点的距离一定会不变.反之一样.

  考虑向左走或者向右走,有一种贪心策略,若向左/右碰到一个人的增加后的点燃时间比原来的时间长,那么我们一定向那个方向走.

  能从\([l,r]\)扩展到\([lx,r]\)的条件一定是走\([lx,l]\)时烟花棒都保持点燃也就是\(pos[r]-pos[i]\leq 2TV(r-i),lx\leq i\leq l\)而且\(pos[l]-pos[lx]\leq 2TV(l-lx)\),表示时间会增加.

  当我们碰到无论走左边还是右边,点燃时间都不会增加,可能有人会认为走减少小的一端,其实这样是错的.我们可以从后反推,反推时我们一定要走时间减少的,此时若我们走不了说明一定无解.

代码:
#include<bits/stdc++.h>
using namespace std;
const int inf=1e9;
const int M=1e5+5;
int n,k,t;
int A[M];
LL B[M];
bool check(int mid) {
	for(int i=1; i<=n; ++i)B[i]=A[i]-2LL*t*mid*i;
	if(B[1]<B[n])return false;//一定不合法; 
	int lx=k,rx=k;
	for(int i=k-1; i; --i)if(B[i]>=B[lx])lx=i;
	for(int i=k+1; i<=n; ++i)if(B[i]<=B[rx])rx=i;
	int l=k,r=k;
	while(lx<l||r<rx) {
		bool move=false,flag=false;
		int tmpl=l,tmpr=r;
		while(tmpl>lx&&B[tmpl-1]>=B[r])if(B[--tmpl]>=B[l]){flag=true;break;}
		if(flag)move=true,l=tmpl;
		flag=false;
		while(tmpr<rx&&B[tmpr+1]<=B[l])if(B[++tmpr]<=B[r]){flag=true;break;}
		if(flag)move=true,r=tmpr;
		if(!move)return false;
	}l=1,r=n;
	while(l<lx||rx<r) {
		bool move=false,flag=false;
		int tmpl=l,tmpr=r;
		while(tmpl<lx&&B[tmpl+1]>=B[r])if(B[++tmpl]>=B[l]){flag=true;break;}
		if(flag)move=true,l=tmpl;
		flag=false;
		while(tmpr>rx&&B[tmpr-1]<=B[l])if(B[--tmpr]<=B[r]){flag=true;break;}
		if(flag)move=true,r=tmpr;
		if(!move)return false;
	}
	return true;
}
int main(){
	n=rd(),k=rd(),t=rd();
	for(int i=1; i<=n; ++i)A[i]=rd();
	int l=0,r=inf/t+1,ans=-1;
	while(l<=r) {
		int mid=l+r>>1;
		if(check(mid))ans=mid,r=mid-1;
		else l=mid+1;
	}printf("%d",ans);
	return 0;
}


「JOISC 2017 Day 2」门票安排

思路:
  • 所有需要反转的区间一定会有交集\([x,y]\).

证明: 如果有两个区间\([l1,r1]\)和\([l2,r2]\)不相交,那么它们反转之后的答案只可能更劣.

  设未反转前的每个点的覆盖数为\(A_i\),反转后每个点的覆盖数为\(B_i\),设\(B_p\)为\(max({B_i,i\in [x,y]})\).

  • 存在一种最优构造使得答案为\(B_p\)或者\(B_p+1\).

证明: 如果存在一个位置\(s\),使得\(B_p\leq B_s-2\),因为\(B_p\)为\(max({B_i,i\in [x,y]})\),所以\(s\)不在这个区间,那么我们可以取消一个\(l=x\)和一个\(r=y\)的反转区间.这样答案会更优.

  考虑二分答案\(mid\),显然反转次数\(cnt=B_p=A_p-mid\)或者\(cnt=B_p+1=A_p-mid+1\).那么我们可以将这两种情况都\(check\)一遍.

  在\(check\)时我们可以确定反转次数\(cnt\),为什么这样可以?因为若\(mid\)偏大,我们确定的\(cnt\)偏小,此时我们可以取消若干反转区间满足答案.若\(mid\)偏小,那么当我们\(cnt\)未取完,我们会默认取\([1,n]\)的区间.

  考虑该怎么选取反转线段,我们从\(1\)扫到\(p\),当前位置为\(pos\),将所有\(l=pos\)的线段丢到优先队列里面,显然前面的点都满足限制,我们尽可能选择\(r\)大的线段.在每个点我们取尽可能少地选择线段.选择的数量\(x\)满足\(A_i-x+(cnt-x)\leq mid\).

  最后我们检验一下\([p+1,n]\)是否满足条件即可.

代码:
#include<bits/stdc++.h>
using namespace std;
const int M=2e5+5;
LL A[M],B[M];
int n,m,p;
struct info {
	int pos;
	LL val;
	bool operator <(const info &_)const {
		return pos<_.pos;
	}
};
vector<info>Q[M];
priority_queue<info>Qur;
bool Check(LL mid,int k) {
	LL cnt=A[p]-mid+k;
	for(int i=p+1; i<=n; ++i)B[i]=0;
	while(!Qur.empty())Qur.pop();
	for(int i=1; i<=p; ++i) {
		for(int j=0; j<Q[i].size(); ++j)if(Q[i][j].pos>=p)Qur.push(Q[i][j]);
		LL sx=(A[i]+cnt-mid+1)/2;
		if(sx<=0)continue;
		while(!Qur.empty()) {
			info C=Qur.top();Qur.pop();
			LL sy=min(C.val,sx);
			C.val-=sy;sx-=sy;B[p+1]-=sy;
			B[C.pos+1]+=sy*2;cnt-=sy*2;
			if(C.val)Qur.push(C);
			if(!sx)break;
		}
		if(sx)return false;
	}
	for(int i=p+1; i<=n; ++i) {
		B[i]+=B[i-1];
		if(B[i]+A[i]>mid)return false;	
	}
	return true;
}
int main() {
	n=rd();m=rd();
	for(int i=1; i<=m; ++i) {
		int l=rd(),r=rd(),x=rd();
		if(l>r)swap(l,r);
		A[l]+=x,A[r]-=x;
		Q[l].push_back((info)<%r-1,x%>);
	}
	for(int i=1; i<=n; ++i) {
		A[i]+=A[i-1];
		if(A[i]>A[p])p=i;
	}
	LL l=0,r=A[p],ans=0;
	while(l<=r) {
		LL mid=l+r>>1;
		if(Check(mid,0)||Check(mid,1))r=mid-1,ans=mid;
		else l=mid+1;	
	}
	printf("%lld",ans);
	return 0;
}

「JOISC 2017 Day 2」火车旅行

思路:

为什么标签贴着LCA,是我对LCA有什么误解吗

  • 从\(x\)到\(y\)的答案等于从\(y\)到\(x\).
  • 每个点只会跳到高度不低于它的点.

  如果一个点\(x\)右边第一个不比它矮的点为\(a\),
  假设最终点位于\(a\)右边,那么\(x\)一定会先跳到\(a\),如果跳到它们之间的某个点\(b\),从这个点往右跳必定会经过\(a\).
  假设最终点位于\(x,a\)之间.那么我们不妨倒着来,从终点\(y\)跳回\(x\).
  我们发现这个跳跃的过程可以用倍增,需要注意我们记录一个点走\(2^i\)步能向右最远到哪,向左最远到哪.因为我们先向右跳再向左跳,可能比单纯的向左跳更远.

代码:
#include<bits/stdc++.h>
using namespace std;
const int M=1e5+5;
int n,k,q;
int A[M];
int x,y;
int L[20][M],R[20][M];
int stk[M];
int main() {
	n=rd();k=rd();q=rd();
	for(int i=1; i<=n; ++i)A[i]=rd();
	int top=0;stk[0]=1;
	for(int i=1; i<=n; ++i) {
		while(top&&A[stk[top]]<A[i])--top;
		L[0][i]=top?stk[top]:i;		
		stk[++top]=i;
	}top=0;stk[0]=n;
	for(int i=n; i; --i) {
		while(top&&A[stk[top]]<A[i])--top;
		R[0][i]=stk[top];
		stk[++top]=i;
	}
	for(int i=1; i<18; ++i)
		for(int j=1; j<=n; ++j) {
			L[i][j]=min(L[i-1][L[i-1][j]],L[i-1][R[i-1][j]]);
			R[i][j]=max(R[i-1][R[i-1][j]],R[i-1][L[i-1][j]]);	
		}
	while(q--) {
		int x=rd(),y=rd();
		if(x>y)swap(x,y);
		int lx=x,rx=x,Ans=0;
		for(int i=17; ~i; --i) {
			int tmpl=min(L[i][lx],L[i][rx]);
			int tmpr=max(R[i][lx],R[i][rx]);
			if(tmpr<y) {
				rx=tmpr;lx=tmpl;
				Ans+=1<<i;
			}
		}x=rx;
		lx=y;rx=y;
		for(int i=17; ~i; --i) {
			int tmpl=min(L[i][lx],L[i][rx]);
			int tmpr=max(R[i][lx],R[i][rx]);
			if(tmpl>x) {
				rx=tmpr;lx=tmpl;
				Ans+=1<<i;
			}	
		}
		printf("%d\n",Ans);
	}
	return 0;
}

「JOISC 2017 Day 3」长途巴士

思路:

  一开始一直往贪心上想,想着按照服务站顺序来,每次淘汰一批人.

  其实贪心是不好贪的,按照上面的思路我们可以将乘客按照\(D_i\)排序,那么对于每一个服务站来说,淘汰的人必定是连续的.

  可不可能存在一种情况使得一个服务站\(x\)先淘汰\([l,r]\)的人,再用一个服务站\(y\)淘汰\([lx,l-1]\)和\([r+1,rx]\)的人?发现这种情况肯定不会比用\(x\)淘汰\([lx,r]\)的人,用\(y\)淘汰\([r+1,rx]\)的人的方案优.

  这样我们就可以写一个\(dp\),\(dp[i]\)表示处理完\([1,i]\)的乘客的最优花费.那么对于\(i\)来说,如果我们不选择以\(i\)为一次淘汰的终点,那么\(dp[i]=dp[i-1]+W*(\lfloor (X-D_i)/T\rfloor+1)\).如果我们选择以\(i\)为一次淘汰的终点,那么我们枚举起点,并且找到一个服务站\(x\)满足\(\lfloor s[x]/T \rfloor\)尽量小,并且\(x\)只会刚好淘汰掉\(i\),即\(D[i]<s[x]\%T<D[i+1]\).

  于是我们的\(dp\)转移方程出来了,\(dp[i]=\min(dp[i],dp[j]+W(i-j)*(\lfloor s[x]/T \rfloor)+sumC[i]-sumC[j])\).这样我们就写出了\(O(n^2)\)代码.

  考虑接着优化,很容易看出来是个斜率优化,化简得\(dp[i]=\min(dp[j]-sumC[j]-Wj*(\lfloor s[x]/T \rfloor))+Wi*(\lfloor s[x]/T \rfloor)+sumC[i]\).那么我们设\(Y[i]=dp[i]-sumC[i],X[i]=Wi\),那么我们用单调栈维护斜率上升,由于\(K=(\lfloor s[x]/T \rfloor)\)不是单调的,所以需要我们在单调栈里二分答案.

代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
LL X,W,T;
namespace P71 {
	const int M=2005;
	const LL inf=1e18;
	struct info {
		int d,c;
		bool operator <(const info &_)const {
			return d<_.d;
		}
	}A[M];
	LL sum[M],dp[M];
	LL Mi[M],s[M];
	void Solve() {
		for(int i=1; i<=n; ++i)s[i]=rd();s[++n]=X;
		sort(s+1,s+n+1);
		for(int i=1; i<=m; ++i)A[i].d=rd(),A[i].c=rd();
		sort(A+1,A+m+1);
		for(int i=1; i<=m; ++i)sum[i]=sum[i-1]+A[i].c;
		for(int i=1; i<=m; ++i) {
			Mi[i]=inf;
			for(int j=1; j<=n; ++j)
				if(A[i].d<s[j]%T&&(i==m||s[j]%T<A[i+1].d)){Mi[i]=s[j]/T;break;}
		}
		for(int i=1; i<=m; ++i) {
			dp[i]=dp[i-1]+W*((X-A[i].d)/T+1);
			if(Mi[i]==inf)continue;
			for(int j=i-1; ~j; --j)
				Min(dp[i],dp[j]+Mi[i]*(i-j)*W+sum[i]-sum[j]);
		}printf("%lld",dp[m]+W*(X/T+1));
	}
}
namespace P100 {
	const int M=2e5+5;
	const LL inf=1e18;
	struct info {
		int d,c;
		bool operator <(const info &_)const {
			return d<_.d;
		}
	}A[M];
	LL sum[M],dp[M];
	LL Mi[M],s[M];
	bool cmp(LL x,LL y) {return x%T<y%T;}
	void Solve_Mi() {
		int k=1;
		for(int i=1; i<=m; ++i) {
			Mi[i]=inf;
			while(k<=n&&s[k]%T<A[i].d)++k;
			while(k<=n&&(i==m||s[k]%T<A[i+1].d))Min(Mi[i],s[k]/T),++k;
		}
	}
	int stk[M],top;
	LL x[M],y[M];
	bool Judge(int a,int b,int c) {return 1.0*(y[c]-y[b])/(x[c]-x[b])<=1.0*(y[b]-y[a])/(x[b]-x[a]);}
	LL Calc(int pos,LL K) {return y[pos]-x[pos]*K;}
	LL Query(LL K) {
		int l=2,r=top,res=1;
		while(l<=r) {
			int mid=l+r>>1;
			if(Calc(stk[mid-1],K)>=Calc(stk[mid],K))res=mid,l=mid+1;
			else r=mid-1;
		}
		return Calc(stk[res],K);
	}
	void Solve() {
		for(int i=1; i<=n; ++i)s[i]=rd();s[++n]=X;
		sort(s+1,s+n+1,cmp);
		for(int i=1; i<=m; ++i)A[i].d=rd(),A[i].c=rd();
		sort(A+1,A+m+1);
		for(int i=1; i<=m; ++i)sum[i]=sum[i-1]+A[i].c;
		Solve_Mi();stk[++top]=0;
		for(int i=1; i<=m; ++i) {
			dp[i]=dp[i-1]+W*((X-A[i].d)/T+1);
			if(Mi[i]^inf)Min(dp[i],Query(Mi[i])+sum[i]+Mi[i]*i*W);
			y[i]=dp[i]-sum[i];x[i]=W*i;
			while(top>1&&Judge(stk[top-1],stk[top],i))--top;
			stk[++top]=i;
		}printf("%lld",dp[m]+W*(X/T+1));
	}
}
int main() {
	X=rd();n=rd();m=rd();W=rd();T=rd();
	if(n<=2000)P71::Solve();
	else P100::Solve();
	return 0;
}


「JOISC 2017 Day 3」幽深府邸

思路:

  考虑求出一个人向左最多到达何处,向右最多到达何处.一个人可能先向右走拿到钥匙,再往左走打开门,就是会来来回回走.
  如果一个人只往一个方向走,那么我们先预处理出每一扇门左边和右边离它最近的有它钥匙的房间位置.如果对于每个起始点我们都一步一步走,那么复杂度为\(O(n^2)\),我们可以发现如果我们走到\(pos\),那么\(pos\)能走到的点,这个点一定能走到.所以我们可以直接跳,那么复杂度为\(O(n)\).
  但是由于这个人会左右横跳,我们继续思考上面的思路,我们可以同时向左和向右扩展.

while(l[i]>1||r[i]<n) {
	bool Move=false;
	while(l[i]>1&&Y[l[i]-1]<=r[i])Move=true,Max(r[i],r[l[i]-1]),Min(l[i],l[l[i]-1]);
	if(r[i]<n&&X[r[i]]>=l[i])Move=true,Min(l[i],l[r[i]+1]),Max(r[i],r[r[i]+1]);
	if(!Move)break;
}

  这个复杂度好像无法保证.(数据太水了,让我过了.)
  我们可以发现上面的代码主要是跑右边的时候,由于没有处理过,导致我们都是一步一步跳,相当于暴力.考虑使用\(dfs\),当我们到达一个点的时候,我们先将这个点的\(l,r\)跑出来,这样可以大大地提高效率.
  复杂度玄学(就是我不知道),网上说是\(n\log n\),我个人感觉是\(O(n)\),具体我说不清.

代码:
#include<bits/stdc++.h>
using namespace std;
const int M=5e5+5;
int n;
int A[M],C[M];
vector<int>Q[M];
int X[M],Y[M];
int l[M],r[M];
bool vis[M];
void dfs(int i) {
	if(vis[i])return;
	vis[i]=true;
	while(l[i]>1||r[i]<n) {
		bool Move=false;
		if(l[i]>1&&Y[l[i]-1]<=r[i]) {
			dfs(l[i]-1);
			Move=true,Max(r[i],r[l[i]-1]),Min(l[i],l[l[i]-1]);
		}
		if(r[i]<n&&X[r[i]]>=l[i]) {
			dfs(r[i]+1);
			Move=true,Min(l[i],l[r[i]+1]),Max(r[i],r[r[i]+1]);
		}
		if(!Move)break;
	}
}
int main() {
	n=rd();
	for(int i=1; i<n; ++i)C[i]=rd();
	for(int i=1; i<=n; ++i) {
		int cnt=rd();
		for(int j=1; j<=cnt; ++j) {
			int x=rd();
			Q[i].push_back(x);A[x]=i;
		}
		X[i]=A[C[i]];
	}
	for(int i=1; i<=n; ++i)A[i]=n+1;
	for(int i=n; i; --i) {
		for(int j=0; j<Q[i].size(); ++j)A[Q[i][j]]=i;
		Y[i-1]=A[C[i-1]];
	}
	for(int i=1; i<=n; ++i)l[i]=r[i]=i;
	for(int i=1; i<=n; ++i)dfs(i);
	int q=rd();	
	while(q--) {
		int x=rd(),y=rd();
		if(l[x]<=y&&y<=r[x])puts("YES");
		else puts("NO");
	}
	return 0;
}

「JOISC 2017 Day 4」绑架 2

思路:

  对于这辆车会在哪个路口转弯,我们可以通过倍增求解.难点在于如何处理往左转还是往右转.
  思考了好久没想到如何处理,我们暴力枚举往哪个方向转,大胆猜测一波状态数是\(O(n)\)级别的.
  然而我只会猜,不会证.(以后再来修)

代码:
#include<bits/stdc++.h>
using namespace std;
const int M=5e4+5;
int n,m,q;
int A[16][M],B[16][M];
map<int,LL>Mp[2][M];
LL Solve(int x,int y,int cur) {
	if(x<1||x>n||y<1||y>m)return -1;
	if(Mp[cur][x][y])return Mp[cur][x][y];
	LL ans=0;
	if(!cur) {
		int posx=x-1;
		for(int i=15; ~i; --i)if(posx>=(1<<i)&&A[i][posx-(1<<i)+1]<B[0][y])posx-=1<<i;	
		Max(ans,Solve(posx,y,cur^1)+x-posx);
		
		posx=x+1;
		for(int i=15; ~i; --i)if(posx+(1<<i)<=n+1&&A[i][posx]<B[0][y])posx+=1<<i;
		Max(ans,Solve(posx,y,cur^1)+posx-x);
	}else {
		int posy=y-1;
		for(int i=15; ~i; --i)if(posy>=(1<<i)&&B[i][posy-(1<<i)+1]<A[0][x])posy-=1<<i;	
		Max(ans,Solve(x,posy,cur^1)+y-posy);
		
		posy=y+1;
		for(int i=15; ~i; --i)if(posy+(1<<i)<=m+1&&B[i][posy]<A[0][x])posy+=1<<i;
		Max(ans,Solve(x,posy,cur^1)+posy-y);
	}
	return Mp[cur][x][y]=ans;
}
int main() {
	n=rd(),m=rd(),q=rd();
	for(int i=1; i<=n; ++i)A[0][i]=rd();
	for(int i=1; i<=m; ++i)B[0][i]=rd();
	for(int i=1; i<16; ++i) {
		for(int j=1; j+(1<<i)-1<=n; ++j)
			A[i][j]=max(A[i-1][j],A[i-1][j+(1<<i-1)]);
		for(int j=1; j+(1<<i)-1<=m; ++j)
			B[i][j]=max(B[i-1][j],B[i-1][j+(1<<i-1)]);	
	}
	while(q--) {
		int x=rd(),y=rd();
		printf("%lld\n",max(Solve(x,y,0),Solve(x,y,1)));	
	}
	return 0;
}
上一篇:题解 loj2736 【「JOISC 2016 Day 3」回转寿司】


下一篇:MySQL45讲——日志系统:一条SQL更新语句是如何执行的 学习笔记