JOIST2025 VP&补题记录

Day1

\(12+35+100=147\)

T1 糖了没想到多项式做法。

T1: Exhibition 3

题意

给定一个长度为 \(n\) 的序列 \(a\),给定 \(m\) 个区间 \(l_i,r_i\),你需要重排 \(a\),令 \(b_i=\max\limits_{j=l_i}^{r_i} a_j\),你需要使 \((b_1,b_2,\dots,b_m)\) 字典序最大,输出最大字典序。

\(1\le n,m\le 10^5\)。

solution

深深感受到自己的弱小。

考虑从大到小枚举每种 \(a\) 的权值,并让当前这种权值尽可能覆盖前面的还未被覆盖过的区间。

于是有了最朴素的暴力:枚举每种权值,令这种权值在 \(a\) 中出现 \(cnt\) 次,按编号从小到大考虑每个未被覆盖的区间,若加入后,覆盖已选的全部区间(含新加入的)所需点数不超过 \(cnt\),就将其加入,判断覆盖一组区间所需的最少点数可以按照右端点升序排序,并依次在未被覆盖区间的最小右端点上放一个点进行覆盖,根据贪心可以知道这是对的。

考虑合法的区间是什么样的,不难发现合法的区间由一段前缀以及后面一些零散区间构成,看起来这并没有什么可用的信息,但实际上覆盖这段前缀所需的点数恰好为 \(cnt\),否则下一个未被覆盖的区间一定会被选入,这也就说明了后面的零散区间不会改变覆盖所需要的最小点数。

考虑如何找到这个前缀,目前我们有了 \(O(c\log c)\) 判定 \(c\) 个区间是否可行的方法,于是考虑倍增,令当前第一个未被覆盖区间编号为 \(x\),找到最大的 \(k\) 满足覆盖编号在 \(x,x+2\^k-1\) 的所有区间所需要的点数不超过 \(cnt\),且覆盖编号在 \(x,x+2\^{k+1}-1\) 的所有区间所需要的点数超过 \(cnt\),判定是否可行直接使用暴力的判定方法,然后在 \([x+2^k-1,x+2^{k+1}-1)\) 内二分出最长的合法前缀。

这个做法看起来很神秘,但是这样做能保证复杂度,设这段前缀长度为 \(c\),通过倍增,我们用 \(O(c\log c)\) 的时间确定了二分范围,二分进行了 \(O(\log c)\) 次,每次判定需要时间为 \(O(c\log c)\),所以用 \(O(c\log^2 c)\) 的时间找到了这段前缀,由于每个区间只被覆盖一次,所以相当于我们用了 \(O(c\log^2 c)\) 的时间覆盖了 \(O(c)\) 个区间,复杂度是正确的,个人认为这个倍增非常巧妙。

前缀确定后,还需要找出所有加入后所需点数不增加的零散区间,考虑如何找到这些区间。

一个经典的结论:按照上面的贪心,我们得到 \(cnt\) 个点最右边可以放置到的位置,反过来,按照左端点降序排序,找到最大的未被覆盖区间的左端点并在端点上放一个点覆盖此区间,得到 \(cnt\) 个点最左边可以放置到的位置,令第 \(i\) 个点最左边与最右边分别能放到 \(pL_i,pR_i\),那么这个点放在 \(pL_i,pR_i\) 的任何一个位置,都能通过调整其它的点来做到覆盖所有区间。

并且 \(pL_i,pR_i\) 是两两无交的,否则将点放在交集会使点数减少。

那么可以得到:\(L,R\) 加入后不会增加点数,当且仅当 \(L,R\) 与某个 \(pL,pR\) 有交。

将这些区间分成以下几类:

  1. \(L,R\) 完全包含其中一个 \(pL,pR\);
  2. \(L,R\) 不包含任何一个 \(pL,pR\),且与两个 \(pL,pR\) 有交;
  3. \(L,R\) 不包含任何一个 \(pL,pR\),且与一个 \(pL,pR\) 有交。

加入一个区间后,可能对某些 \(pL,pR\) 有影响,但是一定是使某些 \(pL,pR\) 变小,因为限制更严了。

考虑加入上面每种区间的影响。

对于第一种区间,根据上面的贪心,它并不会影响 \(pL,pR\),因为按照右端点贪心时,会先在与它有交的 \(pR\) 处放一个点,从而将 \(L,R\) 覆盖,不会参与后面的贪心,\(pL\) 同理,并且发现无论 \(pL,pR\) 怎么缩小,\(L,R\) 一定包含 \(pL,pR\),所以这类区间无论如何都可以被加入,可以放到最后同时处理,由于都会加入,所以不用考虑编号,用线段树维护 \(L<pL\land pR<R\) 的二维偏序即可。

对于第二种区间,有 \(pL_{j}\le L\le pR_{j}<pL_{j+1}\le R\le pR_{j+1}\),那么根据上面的贪心,在 \(pR_j\) 处放置点时将 \(L,R\) 覆盖,在 \(pL_{j+1}\) 处放置点时将 \(L,R\) 覆盖,所以 \(pL,pR\) 并不会变动。

对于第三种区间,若左端点落在 \(pL,pR\) 内,则需要将 \(pL\leftarrow L\),若右端点落在 \(pL,pR\) 内,则需要将 \(pR\leftarrow R\),同时当前 \(pL,pR\) 修改后,后面的 \(pR\) 或前面的 \(pL\) 可能也会被修改。

先考虑如何找到编号最小的第二种和第三种区间,发现这两种区间必然有端点落在某个 \(pL,pR\) 上,于是用线段树在每个端点处维护编号即可,对所有 \(pL,pR\) 找到端点在里面的最小编号,放进小根堆中每次取出堆顶更新,并在 \(pL,pR\) 修改后重新获取最小的编号即可。

再来考虑 \(pR\) 的更新,\(pL\) 的情况是对称的。

若 \(pR_j\) 更新,则 \(pR_{j+1}\) 会变成所有 \(L>pR_j\) 的区间中 \(R\) 的最小值,令 \(c\) 为当前权值覆盖区间的总数量,由于每个区间对 \(pR_j\) 的更新最多贡献一次,所以 \(pR\) 的总修改量均摊只有 \(O(c)\),用线段树维护加入区间中每个 \(L\) 对应的最小 \(R\),在更新 \(pR_j\) 时检查 \(pR_{j+1}\) 是否需要更新,若需要则递归更新 \(pR_{j+1}\),否则停止更新,因为 \(j+1\) 后面点的 \(pR\) 不可能继续更新。

注意从堆中取出区间时要判断是否与 \(pL,pR\) 还有交,因为 \(pL,pR\) 可能已经修改过了。

最终时间复杂度 \(O(n\log^2 n)\),精细实现预处理出 \(2^k\) 长度的排序可以做到 \(O(n\log n)\)。

代码为 \(O(n\log^2 n)\),一些地方与题解略有差异,实现上有很多细节,码量也很大,最好整理清楚思路再写。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
#include<queue>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m;
int a[N],ans[N];
bitset<N> vis;

struct Node{int l,r,id;}p[N];
inline bool cmp1(Node x,Node y) {return x.r<y.r;}
inline bool cmp2(Node x,Node y) {return x.l>y.l;}
inline bool cmp3(Node x,Node y) {return x.id<y.id;}

inline bool check(vector<Node> &tmp,int mxt) {
	sort(tmp.begin(),tmp.end(),cmp1);
	int cnt=0,pR=0;
	for (Node x:tmp) if (x.l>pR) cnt++,pR=x.r;
	sort(tmp.begin(),tmp.end(),cmp3);
	return cnt<=mxt;
}

struct Segment_Tree1 {
	// find L<pl and pr<R [L,R]->L mx=max(R)
	int mx[N<<2];
	vector<Node> tr[N];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void build(int l,int r,int now) {
		if (l==r) return mx[now]=(tr[l].empty()?0:tr[l].back().r),void();
		int mid=l+r>>1;
		build(l,mid,ls(now));
		build(mid+1,r,rs(now));
		mx[now]=max(mx[ls(now)],mx[rs(now)]);
	}
	
	inline void build() {
		sort(p+1,p+1+m,cmp1);
		f(i,1,m) tr[p[i].l].push_back(p[i]);
		sort(p+1,p+1+m,cmp3);
		build(1,n,1);
	}
	
	inline int query(int x,int y,int l,int r,int now,int k) {
		if (mx[now]<=k) return 0;
		if (l==r) {
			while (!tr[l].empty()&&vis[tr[l].back().id]) tr[l].pop_back();
			mx[now]=(tr[l].empty()?0:tr[l].back().r);
			if (mx[now]<=k) return 0;
			int ret=tr[l].back().id;
			tr[l].pop_back();
			mx[now]=(tr[l].empty()?0:tr[l].back().r);
			return ret;
		}
		int mid=l+r>>1,ret=0;
		if (x<=mid) ret=query(x,y,l,mid,ls(now),k);
		if (!ret&&y>mid) ret=query(x,y,mid+1,r,rs(now),k);
		mx[now]=max(mx[ls(now)],mx[rs(now)]);
		return ret;
	}
}tr1;

struct Segment_Tree2 {
	// find pl<=L<=pr or pl<=R<=pr id min [L,R]->L,R
	int mn[N<<2];
	vector<int> tr[N];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void build(int l,int r,int now) {
		if (l==r) return mn[now]=(tr[l].empty()?inf:tr[l].back()),void();
		int mid=l+r>>1;
		build(l,mid,ls(now));
		build(mid+1,r,rs(now));
		mn[now]=min(mn[ls(now)],mn[rs(now)]);
	}
	
	inline void build() {
		f(i,1,(n<<2)) mn[i]=inf;
		F(i,m,1) {
			tr[p[i].l].push_back(p[i].id);
			tr[p[i].r].push_back(p[i].id);
		}
		build(1,n,1);
	}
	
	inline void update(int x,int l,int r,int now) {
		if (l==r) {
			while (!tr[l].empty()&&vis[tr[l].back()]) tr[l].pop_back();
			mn[now]=(tr[l].empty()?inf:tr[l].back());
			return;
		}
		int mid=l+r>>1;
		if (x<=mid) update(x,l,mid,ls(now));
		else update(x,mid+1,r,rs(now));
		mn[now]=min(mn[ls(now)],mn[rs(now)]);
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (x<=l&&y>=r) return mn[now];
		int mid=l+r>>1,res=inf;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res=min(res,query(x,y,mid+1,r,rs(now)));
		return res;
	}
}tr2;

struct Segment_Tree3 {
	//update [pl,pr] [L,R] L=x R min
	int mx[N<<2];
	inline void build() {f(i,1,(n<<2))mx[i]=inf;}
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void update(int x,int l,int r,int now,int k) {
		if (l==r) {
			if (k!=inf) mx[now]=min(mx[now],k);
			else mx[now]=inf;
			return;
		}
		int mid=l+r>>1;
		if (x<=mid) update(x,l,mid,ls(now),k);
		else update(x,mid+1,r,rs(now),k);
		mx[now]=min(mx[ls(now)],mx[rs(now)]);
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (x<=l&&y>=r) return mx[now];
		int mid=l+r>>1,res=inf;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res=min(res,query(x,y,mid+1,r,rs(now)));
		return res;
	}
}tr3;

struct Segment_Tree4 {
	//update [pl,pr] [L,R] R=x L max
	int mx[N<<2];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void update(int x,int l,int r,int now,int k) {
		if (l==r) {
			if (k!=0) mx[now]=max(mx[now],k);
			else mx[now]=0;
			return;
		}
		int mid=l+r>>1;
		if (x<=mid) update(x,l,mid,ls(now),k);
		else update(x,mid+1,r,rs(now),k);
		mx[now]=max(mx[ls(now)],mx[rs(now)]);
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (x<=l&&y>=r) return mx[now];
		int mid=l+r>>1,res=0;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res=max(res,query(x,y,mid+1,r,rs(now)));
		return res;
	}
}tr4;

struct Que_Node {
	int id,pid;
	inline bool operator <(const Que_Node &x)const {return id>x.id;}
};

inline void insert_id(vector<Node> &tmp,int id) {
	vis.set(id);
	tmp.push_back(p[id]);
	tr2.update(p[id].l,1,n,1);
	tr2.update(p[id].r,1,n,1);
	tr3.update(p[id].l,1,n,1,p[id].r);
	tr4.update(p[id].r,1,n,1,p[id].l);
}

inline bool check_cap(int id,int pid,int *pL,int *pR) {
	if (pL[pid]>p[id].r||p[id].l>pR[pid]) return 0;
	return 1;
}

inline void update_pL(int id,int *pL,int *pR,int k,priority_queue<Que_Node> &q) {
	int new_id=tr2.query(pL[id],pR[id],1,n,1);
	if (new_id!=inf) q.push({new_id,id});
	if (id==1||pL[id]==1) return;
	int mxL=tr4.query(1,pL[id]-1,1,n,1);
	if (mxL>pL[id-1]) pL[id-1]=mxL,update_pL(id-1,pL,pR,k,q);
}

inline void update_pR(int id,int *pL,int *pR,int k,priority_queue<Que_Node> &q) {
	int new_id=tr2.query(pL[id],pR[id],1,n,1);
	if (new_id!=inf) q.push({new_id,id});
	if (id==k||pR[id]==n) return;
	int mnR=tr3.query(pR[id]+1,n,1,n,1);
	if (pR[id+1]>mnR) pR[id+1]=mnR,update_pR(id+1,pL,pR,k,q);
}

inline void solve(vector<Node> &tmp,int k,int val) {
	for (Node &k:tmp) {
		int id=k.id;
		vis.set(k.id);
		tr2.update(p[id].l,1,n,1);
		tr2.update(p[id].r,1,n,1);
		tr3.update(p[id].l,1,n,1,p[id].r);
		tr4.update(p[id].r,1,n,1,p[id].l);
	}
	static int pL[N],pR[N];
	int now=0;
	sort(tmp.begin(),tmp.end(),cmp1);
	for (Node x:tmp) if (x.l>pR[now]) pR[++now]=x.r;
	now=k;
	sort(tmp.begin(),tmp.end(),cmp2);
	for (Node x:tmp) if (now==k||x.r<pL[now+1]) pL[now--]=x.l;
	priority_queue<Que_Node> q;
	f(i,1,k) {
		int id=tr2.query(pL[i],pR[i],1,n,1);
		if (id!=inf) q.push({id,i});
	}
	while (!q.empty()) {
		int id=q.top().id,pid=q.top().pid;
		q.pop();
		if (vis[id]||!check_cap(id,pid,pL,pR)) {
			int new_id=tr2.query(pL[pid],pR[pid],1,n,1);
			if (new_id!=inf) q.push({new_id,pid});
			continue;
		}
		insert_id(tmp,id);
		int new_id=tr2.query(pL[pid],pR[pid],1,n,1);
		if (new_id!=inf) q.push({new_id,pid});
		if (pL[pid]<=p[id].l&&p[id].r<=pR[pid]) {
			if (pL[pid]!=p[id].l) pL[pid]=p[id].l,update_pL(pid,pL,pR,k,q);
			if (pR[pid]!=p[id].r) pR[pid]=p[id].r,update_pR(pid,pL,pR,k,q);
			continue;
		}
		if (pL[pid]<=p[id].l&&p[id].l<=pR[pid]) {
			if (pid<k&&check_cap(id,pid+1,pL,pR)) continue; 
			if (pL[pid]!=p[id].l) pL[pid]=p[id].l,update_pL(pid,pL,pR,k,q);
		}
		if (pL[pid]<=p[id].r&&p[id].r<=pR[pid]) {
			if (pid>1&&check_cap(id,pid-1,pL,pR)) continue;
			if (pR[pid]!=p[id].r) pR[pid]=p[id].r,update_pR(pid,pL,pR,k,q);
		}
	}
	f(i,1,k) {
		int L=pL[i],R=pR[i];
		if (L==1) continue;
		int id=tr1.query(1,L-1,1,n,1,R);
		while (id) {
			insert_id(tmp,id);
			id=tr1.query(1,L-1,1,n,1,R);
		}
	}
	for (Node &k:tmp) {
		tr3.update(k.l,1,n,1,inf);
		tr4.update(k.r,1,n,1,0);
		ans[k.id]=val;
	}
}

int check_id=1;
inline void solve(int mxt,int val) {
	int now=check_id,pt=now-1;
	vector<Node> tmp;
	for (int k=1,pos=0;(pos=now+k-1)<=m;k<<=1) {
		f(i,pt+1,pos) if (!vis[i]) tmp.push_back(p[i]);
		if (check(tmp,mxt)) pt=pos;
		else {
			f(i,pt+1,pos) if (!vis[i]) tmp.pop_back();
			break;
		}
	}
	int l=pt,r=min(m,pt+(pt-now+1));
	while (l<=r) {
		int mid=l+r>>1;
		f(i,pt+1,mid) if (!vis[i]) tmp.push_back(p[i]);
		if (check(tmp,mxt)) l=mid+1,pt=mid;
		else {
			f(i,pt+1,mid) if (!vis[i]) tmp.pop_back();
			r=mid-1;
		}
	}
	if (pt==m) {
		for (Node k:tmp) vis.set(k.id),ans[k.id]=val;
		while (vis[check_id]) check_id++;
		return;
	}
	solve(tmp,mxt,val);
	while (vis[check_id]) check_id++;
}

int main() {
	read(n);read(m);
	f(i,1,n) {int x;read(x);a[x]++;}
	f(i,1,m) read(p[i].l),read(p[i].r),p[i].id=i;
	tr1.build();tr2.build();tr3.build();
	F(i,n,1) if (a[i]) solve(a[i],i);
	f(i,1,m) printf("%d\n",ans[i]);
	return 0;
}

T2: Fortune Telling 3

solution

极致的状态压缩,太牛了这个题。

L=18

此题不好使用二进制来维护信息,因为 \(0,1\) 的个数不确定,考虑换一种表示方法,一个基础的想法就是用 \(x\) 个 \(0\) 与 \(y\) 个 \(1\) 表示 \(\binom{x+y}{x}\) 级别的信息。

我们想要传递一个组合数的形式,那么可以用隔板法传递一个数组,具体地,我们可以传递一个长度为 \(x+1\) 的数组,表示第一个 \(0\) 前面,从左到右两个 \(0\) 之间,最后一个 \(0\) 后面有几个 \(1\)。

但是我们得到的数可能是 \(0\) 也可能是 \(1\) 怎么办呢,考虑在前 \(890\) 个位置拿出 \(4\) 个 \(0\) 和 \(4\) 个 \(1\),\(0\) 放在前 \(1\) 放在后,我们想要传递的数是前 \(890\) 个数中 \(1\) 的个数,那么考虑后 \(10\) 个位置,若值为 \(0\) 则放到后面的 \(1\) 之间,若值为 \(1\) 则放到前面的 \(0\) 之间,解码时找到前面四个 \(0\) 和后面四个 \(1\),前面以 \(0\) 为界得到 \(5\) 个位置 \(1\) 的数量,后面以 \(1\) 为界得到 \(5\) 个位置 \(0\) 的数量,将每个位置数量相加,就可以得到一个长度为 \(5\),和为 \(10\) 的数组,能够表示 \(\binom{14}{4}>1000\) 的信息,对于后面 \(10\) 个位置的 \(1\),都出现在前面 \(0\) 的旁边,直接计入贡献即可。

如果前 \(890\) 个位置找不到 \(4\) 个 \(0\),则将后 \(10\) 个位置所有 \(1\) 放入,解码时发现 \(0\) 的个数不到 \(4\) 个,于是通过给出 \(1\) 的数量可以计算出所有 \(1\) 的数量。

前面找不到 \(4\) 个 \(1\) 的情况同理,这样做可以做到 \(L=18\),获得 \(71\) 的分数。

我认为打到 \(71\) 的分数已经很困难了,下面的优化有点不是人类了。
Anna

cpp 复制代码
#include "Anna.h"
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)

inline vector<int> dfs(int &num,int sum,int pos,vector<int> now) {
	if (pos==5) {
		num--;
		now.push_back(sum);
		if (!num) return now;
		return vector<int>();
	}
	f(i,0,sum) {
		now.push_back(i);
		vector<int> ret=dfs(num,sum-i,pos+1,now);
		if (!ret.empty()) return ret;
		now.pop_back();
	}
	return vector<int>();
}

void Anna(int N) {
	int c0=0,c1=0,Len=0;
	int now=DrawCard(-1),num=0;
	f(i,1,890) {
		if (now) {
			num++;
			if (c1<4) now=DrawCard(Len++),c1++;
			else now=DrawCard(-1);
		}
		else {
			if (c0<4) now=DrawCard(0),Len++,c0++;
			else now=DrawCard(-1);
		}
	}
	if (c0<4) {
		f(i,1,10) {
			if (now) now=DrawCard(Len++);
			else now=DrawCard(-1);
		}
		return;
	}
	if (c1<4) {
		f(i,1,10) {
			if (now) now=DrawCard(-1);
			else now=DrawCard(Len++);
		}
		return;
	}
	num++;
	vector<int> tmp=dfs(num,10,1,vector<int>());
	int p1=0,p2=4,pos=0;
	while (!tmp[pos]) pos++,p1++,p2++;
	f(i,1,10) {
		if (now) now=DrawCard(p1++),p2++;
		else now=DrawCard(p2++);
		tmp[pos]--;
		while (pos<=4&&!tmp[pos]) pos++,p1++,p2++;
	}
}

Bruno

cpp 复制代码
#include "Bruno.h"
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)

inline int dfs(vector<int> &ans,int sum,int pos,vector<int> now,int &ret) {
	if (pos==5) {
		ret++;
		now.push_back(sum);
		f(i,0,4) if (ans[i]!=now[i]) return -1;
		return ret;
	}
	f(i,0,sum) {
		now.push_back(i);
		int res=dfs(ans,sum-i,pos+1,now,ret);
		if (res!=-1) return res;
		now.pop_back();
	}
	return -1;
}

int Bruno(int N, int L, std::vector<int> C) {
	int c0=0,c1=0;
	for (int x:C) {
		if (x) c1++;
		else c0++;
	}
	if (c0<4) return 886-c0+c1;
	if (c1<4) return 14-c0+c1;
	vector<int> A,B;
	c0=0;c1=0;int p0=0,p1=0;L--;
	f(i,0,L) {
		if (!C[i]) c0++;
		if (c0==4) {p0=i;break;}
	}
	F(i,L,0) {
		if (C[i]) c1++;
		if (c1==4) {p1=i;break;}
	}
	c0=0;c1=0;
	int num=0;
	f(i,0,p1-1) {
		if (!C[i]) A.push_back(num),c0++,num=0;
		else num++;
		if (c0==5) break;
	}
	if (c0==4) A.push_back(num),num=0;
	p0++;
	while (p0<p1&&C[p0]) p0++;
	f(i,p0,L) {
		if (C[i]) B.push_back(num),c1++,num=0;
		else num++;
		if (c1==5) break;
	}
	if (c1==4) B.push_back(num),num=0;
	vector<int> tmp;
	f(i,0,4) tmp.push_back(A[i]+B[i]);
	int sum1=0;
	for (int x:C) sum1+=x;
	return sum1-5+dfs(tmp,10,1,vector<int>(),num);
}
L=16

在前面的表示中,用了固定的 \(10\) 个 \(1\) 和 \(4\) 个 \(0\) 来表示 \(\binom{14}{4}\) 的状态,我们发现可以不固定组合数中 \(1\) 的数量,从而表示更多的状态。

具体地,从前面 \(892\) 个位置拿出 \(4\) 个 \(0\) 和 \(4\) 个 \(1\),要表示的数仍然为前面 \(892\) 个位置中 \(1\) 的数量,令这个数量为 \(c\)。

按顺序考虑 \(892\) 后面的每一个位置,令后面还剩下 \(lst\) 个位置,那么使用后面能够表示的状态数即为 \(\binom{lst+4}{4}\),若 \(c\ge \binom{lst+4}{4}\),则 \(c\leftarrow c-\binom{lst+4}{4}\),再将这个位置扔掉,由于这个位置可能是 \(1\),所以为了计入扔掉位置的贡献,当扔掉位置上是 \(1\) 时将这个贡献计入进 \(c\) 中,即 \(c\leftarrow c+1\)。

解码时和上面类似,解出一个数组后,这个数组的和即为实际插入的牌数,由此可知弃掉了多少张,及每张弃掉时减去的 \(\binom{lst+4}{4}\) 是多少,从而还原贡献,再加上桌上插入的那些牌中 \(1\) 的个数和这个数组本身对应的值即为答案,被弃掉的牌上的 \(1\) 已计入 \(c\) 中,无需额外考虑。

corner case 的解决方案和 \(L=18\) 是一样的。

这种方法能够表示 \(\sum\limits_{i=1}^{8}\binom{4+i}{4}=1286\) 的状态,即使最后扔掉牌使 \(c\) 变化会带来一定损耗,但解决 \(N=900\) 还是绰绰有余的。

做到了 \(L=16\),可以获得 \(81\) 的分数。

L=14

还能优化的地方在于,可以不固定组合数中 \(0\) 的数量。

首先还是从牌中拿出四个 \(0\),这四个 \(0\) 必须放的原因是需要确定一个基本的相对位置,否则难以找到一个标准来得到这个数组。

考虑前 \(890+k\) 张牌,其中 \(k\) 为拿出 \(1\) 的数量,将第 \(D_1,D_2,D_3,D_4\) 个出现的 \(1\) 拿出,若没有则不拿出,其中 \(D_1,D_2,D_3,D_4\) 由拿出 \(1\) 后剩余部分能够表示的状态数决定。

那么根据拿出 \(1\) 的数量,我们可以根据 \(1\) 的数量分为 \(D_1,D_2-1,D_2,D_3-1,D_3,D_4-1,[D_4,\infty )\),四种类别,每种类别利用 \(L=16\) 的方法计算出对应贡献即可。

令拿出 \(1\) 的个数为 \(k\),那么能够表示的信息位有 \(10-k\) 个,那么能够表示的信息量就有 \(\sum\limits_{i=1}^{10-k}\binom{k+i}{k}\)。

\(1\le k\le 4\) 时,四种 \(k\) 合计能表示的状态数有 \(1008\) 种,但是考虑到后面丢弃 \(1\) 带来的损耗,实际上能表示的信息量会略小,根据信息量,保守对 \(D\) 进行取值即可。

解码时找到前 \(4\) 个 \(0\),再考虑后面有几个 \(1\),若第四个 \(0\) 后面的 \(1\) 不超过 \(4\) 个,则它们必然全部用于分隔,否则分隔 \(1\) 的数量为恰好 \(4\),所以 \(1\) 的数量对 \(4\) 取 \(\min\) 的值即为放入作为分隔 \(1\) 的数量,同时也是组合数中 \(0\) 的数量。

和 \(L=16\) 一样解出一个数组,加上数组对应的数值并还原出减去的贡献即可。

若前面全是 \(0\),则后面也全放 \(0\),这样即可解出 \(1\) 的数量,这也是 \(k\) 不取 \(0\) 的原因(会与这种情况冲突)。

若前面拿不出 \(4\) 个 \(0\),那么后面只放 \(1\),同样可以解出 \(1\) 的数量。

于是做到了 \(L=14\),可以通过此题。
Anna

cpp 复制代码
#include "Anna.h"
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)

inline vector<int> dfs(int &num,int sum,int pos,vector<int> now) {
	if (pos==1) {
		num--;
		now.push_back(sum);
		if (!num) return now;
		return vector<int>();
	}
	f(i,0,sum) {
		now.push_back(i);
		vector<int> ret=dfs(num,sum-i,pos-1,now);
		if (!ret.empty()) return ret;
		now.pop_back();
	}
	return vector<int>();
}

void Anna(int N) {
	int _D[]={1,31,165,463};
	int now=DrawCard(-1);
	int c0=0,c1=0,num=0,Len=0;
	f(i,1,890+num) {
		if (now) {
			c1++;
			if (num<4&&c1==_D[num]) now=DrawCard(Len++),num++;
			else now=DrawCard(-1);
		}
		else {
			c0++;
			if (c0<=4) now=DrawCard(0),Len++;
			else now=DrawCard(-1);
		}
	}
	if (c0<4) {
		f(i,895,900) {
			if (now) now=DrawCard(Len++);
			else now=DrawCard(-1);
		}
		return;
	}
	if (!c1) {
		f(i,891,900) {
			if (!now) now=DrawCard(0);
			else now=DrawCard(-1);
		}
		return;
	}
	static int C[15][15];
	f(i,0,13) {C[i][0]=1;f(j,1,i) C[i][j]=C[i-1][j]+C[i-1][j-1];}
	int flag=0,i=890+num+1;c1-=_D[num-1];
	while (i<=900) {
		int lst=900-i+1;
		if (c1>=C[lst+num][num]) c1-=C[lst+num][num];
		else break;
		c1+=now;now=DrawCard(-1);i++;
	}c1++;
	vector<int> tmp=dfs(c1,900-i+1,num+1,vector<int>());
	int p0=0,p1=4,pos=0;
	while (pos<=num&&!tmp[pos]) pos++,p0++,p1++;
	for(;i<=900;i++) {
		if (now) now=DrawCard(p0++),p1++;
		else now=DrawCard(p1++);
		tmp[pos]--;
		while (pos<=num&&!tmp[pos]) pos++,p0++,p1++;
	}
}

Bruno

cpp 复制代码
#include "Bruno.h"
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)

inline int dfs(vector<int> &ans,int sum,int pos,vector<int> now,int &ret) {
	if (pos==1) {
		ret++;
		now.push_back(sum);
		for (int i=0;i<ans.size();i++) if (ans[i]!=now[i]) return -1;
		return ret;
	}
	f(i,0,sum) {
		now.push_back(i);
		int res=dfs(ans,sum-i,pos-1,now,ret);
		if (res!=-1) return res;
		now.pop_back();
	}
	return -1;
}

inline int get_val(int x,int y) {
	static int C[15][15];
	f(i,0,13) {C[i][0]=1;f(j,1,i) C[i][j]=C[i-1][j]+C[i-1][j-1];}
	int ret=0;
	f(i,x+1,10-y) ret+=C[i+y][y];
	return ret;
}

int Bruno(int N, int L, std::vector<int> C) {
	int _D[]={1,31,165,463};
	int c0=0,c1=0;
	for (int x:C) c0+=(!x);
	if (c0<4) return L-c0*2+890;
	if (c0==L) return 14-c0;
	vector<int> A,B;
	c0=0;c1=0;int p0=0,p1=0;L--;
	f(i,0,L) {
		if (!C[i]) c0++;
		if (c0==4) {p0=i;break;}
	}
	F(i,L,p0+1) {
		if (C[i]) c1++,p1=i;
		if (c1==4) {p1=i;break;}
	}
	int len=c1;
	c0=0;c1=0;
	int num=0;
	f(i,0,p1-1) {
		if (!C[i]) A.push_back(num),c0++,num=0;
		else num++;
		if (c0==len+1) break;
	}
	if (c0==len) A.push_back(num),num=0;
	p0++;
	while (p0<p1&&C[p0]) p0++;
	f(i,p0,L) {
		if (C[i]) B.push_back(num),c1++,num=0;
		else num++;
		if (c1==len+1) break;
	}
	if (c1==len) B.push_back(num),num=0;
	int sum1=0,sum=0;
	vector<int> tmp;
	f(i,0,len) tmp.push_back(A[i]+B[i]),sum+=tmp[i];
	for (int x:C) sum1+=x;
	return sum1-len+dfs(tmp,sum,len+1,vector<int>(),num)-1+_D[len-1]+get_val(sum,len);
}

T3: Bitaro's Travel 2

solution

一开始想了个分治套 Kruskal 重构树上维护线段树优化最短路的,后面发现好像没有分治的必要。

直接构建 Kruskal 重构树,一个点通过一次操作能够去到的点就是权值不超过其高度加 \(L\) 的最浅祖先内子树叶子节点,于是直接倍增维护 \(2^k\) 次操作后最浅能到达哪个祖先,并找到最小次数使得起点跳到一个子树内包括终点的祖先节点上即可。

时间复杂度 \(O((n+q)\log n)\),\(n=H\times W\)。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e6+10;
constexpr int M=2e6+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m,q,L,idx;
int mp[N];
struct ques{int sx,sy,ex,ey,id;};
vector<vector<int>> id;

namespace Kruskal {
	int Etot,cnt;
	struct Edge{int u,v,w;}e[M];
	inline bool cmp(Edge x,Edge y) {return x.w<y.w;}
	
	struct dsu {
		int f[N];
		inline int find(int x) {
			if (f[x]==x) return x;
			return f[x]=find(f[x]);
		}
	}ds;
	
	int val[N],dep[N],dfn[N],siz[N],dfn_idx;
	int f[N][22],g[N][22];
	vector<int> T[N];
	
	inline int dfs(int now,int fa) {
		dep[now]=dep[fa]+(siz[now]=1);
		dfn[now]=++dfn_idx;
		if (T[now].empty()) return g[now][0];
		int mx=0;
		for (int v:T[now]) {
			int k=dfs(v,now);
			siz[now]+=siz[v];
			if (!mx||dep[k]<dep[mx]) mx=k;
		}
		return g[now][0]=mx;
	}
	
	inline void solve() {
		static int dx[]={0,0,1,-1};
		static int dy[]={1,-1,0,0};
		f(i,1,n) f(j,1,m) f(k,0,3) {
			int x=i+dx[k],y=j+dy[k];
			if (x<1||x>n||y<1||y>m||id[i][j]>id[x][y]) continue;
			e[++Etot]={id[i][j],id[x][y],max(mp[id[i][j]],mp[id[x][y]])};
		}
		sort(e+1,e+1+Etot,cmp);
		f(i,1,idx*2) ds.f[i]=i;cnt=idx;
		f(i,1,Etot) {
			int u=ds.find(e[i].u),v=ds.find(e[i].v),w=e[i].w;
			if (u==v) continue;
			cnt++;val[cnt]=w;
			f[u][0]=f[v][0]=cnt;
			T[cnt].push_back(u);
			T[cnt].push_back(v);
			ds.f[u]=ds.f[v]=cnt;
		}
		f(j,1,20) f(i,1,cnt) f[i][j]=f[f[i][j-1]][j-1];
		f(i,1,idx) {
			int now=i,D=mp[i]+L;
			F(j,20,0) if (f[now][j]&&val[f[now][j]]<=D) now=f[now][j];
			g[i][0]=now;
		}
		dfs(cnt,0);
		f(j,1,20) f(i,1,cnt) g[i][j]=g[g[i][j-1]][j-1];
	}
	
	inline int query(int x,int y) {
		int ans=0;y=dfn[y];
		F(j,20,0) {
			int fa=g[x][j];
			int L=dfn[fa],R=dfn[fa]+siz[fa]-1;
			if (L<=y&&y<=R) continue;
			ans+=(1<<j);x=g[x][j];
		}
		ans++;x=g[x][0];
		int L=dfn[x],R=dfn[x]+siz[x]-1;
		if (L<=y&&y<=R) return ans;
		return -1;
	}
}

int main() {
	read(n);read(m);read(L);
	id=vector<vector<int>>(n+2,vector<int>(m+2,0));
	f(i,1,n) f(j,1,m) {
		id[i][j]=++idx;
		read(mp[idx]);
	}
	Kruskal::solve();
	read(q);
	while (q--) {
		int sx,sy,ex,ey;
		read(sx);read(sy);read(ex);read(ey);
		printf("%d\n",Kruskal::query(id[sx][sy],id[ex][ey]));
	}
	return 0;
}

Day2

\(0+100+80=180\)

沉迷 T3 四小时写了 8KB 代码。

T1: Ambulance

题意

有一个 \(L\times L\) 的网格,网格四个角落各有一家医院,每家医院有一辆救护车,现在有 \(N\) 个病人需要运送到医院,每辆救护车从其对应医院出发,移动到病人位置将其接上车,再运回其对应医院放下病人,每个时刻只能移动到相邻格子且最多运送一名病人,并且只能将病人运送到其对应的医院,你需要判断能否安排救护车的运送方案使得所有病人在时刻 \(T\) 前均被送到医院。

\(1\le N\le 160,3\le L\le 10^4,1\le T\le 2\times 10^4\)。

solution

赛时看错题了呜呜。

先给医院编号,第一家医院位于 \((1,1)\),第二家位于 \((1,L)\),第三家位于 \((L,1)\),第四家位于 \((L,L)\),由于运送时间为运送距离的两倍,将时间限制 \(T\leftarrow \left\lfloor \frac{T}{2}\right\rfloor\),那么对于每家医院的要求就是运送到这家医院的病人到医院的距离和不超过 \(T\)。

考虑只有第一家和第四家医院的情况,将一个病人 \((x,y)\) 运送到第一家医院距离为 \(x+y-2\),运送到第四家医院距离为 \(2L-(x+y)\),那么按照 \(x+y\) 排序,运送到第一家医院的病人一定是一个前缀,运送到第四家医院的病人一定是一个后缀,否则若存在 \(i>j\) 且 \(i\) 运送到第一家医院,\(j\) 运送到第四家医院,由于 \(x_i+y_i\ge x_j+y_j\),所以将 \(i\) 运送到第四家医院,\(j\) 运送到第一家医院,两家医院的距离和均不增,一定不劣于原方案。

再考虑变成四家医院的情况,依然按照 \(x+y\) 排序并枚举分界点,分界点前面的病人去第一家,第二家或第三家医院,分界点后面的病人去第二家,第三家或第四家医院,再考虑对于分界点前面和后面同样枚举去第二家医院和第三家医院的分界点,以前半部分为例,对于这些病人按照到第二家医院的距离升序排序,那么一个前缀的病人去往第一家或第二家医院,后缀的病人去往第一家或第三家医院一定不劣,证明与上面相同。

令第一部分只能去往第一家或第二家医院,第二部分只能去往第一家或第三家医院,第三部分只能去往第二家或第四家医院,第四部分只能去往第三家或第四家医院。

那么我们需要判定枚举完分界点后是否存在一个安排方案合法,考虑 dp,令:

  • \(f_{1,i,j}\) 表示第一部分从前向后考虑到第 \(i\) 个病人第一家医院的距离和不超过 \(j\),第二家医院的距离和最小值;
  • \(f_{2,i,j}\) 表示第二部分从后向前考虑到第 \(i\) 个病人第一家医院的距离和不超过 \(j\),第三家医院的距离和最小值;
  • \(f_{3,i,j}\) 表示第三部分从前向后考虑到第 \(i\) 个病人第二家医院的距离和不超过 \(j\),第四家医院的距离和最小值;
  • \(f_{4,i,j}\) 表示第四部分从后向前考虑到第 \(i\) 个病人第三家医院的距离和不超过 \(j\),第四家医院的距离和最小值。

转移是简单的,以 \(f_1\) 为例,考虑加入第 \(i\) 个病人,令 \(t_1\) 为当前病人到第一家医院的距离,\(t_2\) 为到第二家医院的距离,那么有转移:\(f_{1,i,j}\leftarrow \min(f_{1,i-1,j}+t_2,f_{1,i-1,j-t_1})\)。

令 \(l_i\) 为第 \(i\) 部分病人数量,枚举第一部分第一家医院使用的距离和 \(t_1\),那么第一家医院在第二部分最多能够承担 \(T-t_1\) 的距离和,第二家医院在第一部分使用的距离和为 \(t_2=f_{1,l_1,t_1}\),第三家医院在第二部分使用的距离和为 \(t_3=f_{2,l_2,T-t_1}\),第四家医院的总距离和为 \(t_4=f_{3,l_3,T-t_2}+f_{4,l_4,T-t_3}\),合法条件即为 \(t_1,t_2,t_3,t_4\le T\)。

那么我们枚举三个分界点时间复杂度为 \(O(n^3)\),每次进行 dp 复杂度为 \(O(nT)\),所以总复杂度为 \(O(n^4T)\),完全不可接受。

考虑优化,首先还是枚举第一个分界点,然后考虑对所有病人按照到第二家医院的距离排序,对所有病人枚举第二个分界点,那么对于一个病人,若他在第一次排序时在第一个分界点前,则他可以去第一家医院,否则他可以去第四家医院,若他在第二次排序时在第二个分界点前,则他可以去第二家医院,否则他可以去第三家医院,这样的策略一定不劣,因为一个病人到第二家医院和到第三家医院的距离和为定值,且交换一个去第二家医院的病人和去第三家医院的病人不会对第一家和第四家医院产生影响,故存在最优方案使去第二家的病人恰好构成按到第二家医院距离排序的全局前缀,即左右两侧的第二分界点由同一个全局分界点同时确定,所以枚举分界点的复杂度就变为 \(O(n^2)\) 了。

dp 时其实只关心第一个分界点,对于第一个分界点分成两部分分别按照到第二家医院的距离排序后,对于两个部分分别对每个前后缀求出对应的 \(f\) 值,枚举第二个分界点后取出对应的前后缀并枚举 \(t_1\) 进行判定即可,枚举第一个分界点时间复杂度为 \(O(n)\),每次 dp 时间复杂度 \(O(nT)\),所以 dp 部分总时间复杂度 \(O(n^2T)\),每次枚举两个分界点后枚举 \(t_1\) 进行判定时间复杂度 \(O(T)\),所以枚举判定部分总时间复杂度 \(O(n^2T)\)。

总时间复杂度 \(O(n^2T)\)。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=160+10;
constexpr int V=1e4+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,L,T,flag_ans;
int f1[N][V],f2[N][V],f3[N][V],f4[N][V];
struct Point{int x,y;}p[N],p1[N],p2[N];

inline int dis1(Point p) {return p.x+p.y-2;}
inline int dis2(Point p) {return L-1+p.x-p.y;}
inline int dis3(Point p) {return L-1-p.x+p.y;}
inline int dis4(Point p) {return L*2-p.x-p.y;}

inline bool cmp1(Point a,Point b) {return dis1(a)<dis1(b);}
inline bool cmp2(Point a,Point b) {return dis2(a)<dis2(b);}

inline void get_dp(int pos) {
	f(j,0,T) f1[0][j]=f2[pos+1][j]=f3[pos][j]=f4[n+1][j]=0;
	f(i,1,pos) {
		int t1=dis1(p[i]),t2=dis2(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f1[i][j]=f1[i-1][j]+t2;
		f(j,t1,T) f1[i][j]=min(f1[i-1][j]+t2,f1[i-1][j-t1]);
	}
	F(i,pos,1) {
		int t1=dis1(p[i]),t2=dis3(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f2[i][j]=f2[i+1][j]+t2;
		f(j,t1,T) f2[i][j]=min(f2[i+1][j]+t2,f2[i+1][j-t1]);
	}
	f(i,pos+1,n) {
		int t1=dis2(p[i]),t2=dis4(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f3[i][j]=f3[i-1][j]+t2;
		f(j,t1,T) f3[i][j]=min(f3[i-1][j]+t2,f3[i-1][j-t1]);
	}
	F(i,n,pos+1) {
		int t1=dis3(p[i]),t2=dis4(p[i]),Rmx=min(T,t1-1);
		f(j,0,Rmx) f4[i][j]=f4[i+1][j]+t2;
		f(j,t1,T) f4[i][j]=min(f4[i+1][j]+t2,f4[i+1][j-t1]);
	}
}

inline bool check(int t1,int pos1,int pos2) {
	int t2=f1[pos1][t1],t3=f2[pos1+1][T-t1];
	if (t2>T||t3>T) return 0;
	int t4=f3[pos2][T-t2]+f4[pos2+1][T-t3];
	return t4<=T;
}

inline void solve(int pos) {
	if (flag_ans) return;
	sort(p+1,p+1+pos,cmp2);
	if (pos!=n) sort(p+1+pos,p+1+n,cmp2);
	get_dp(pos);
	int pos1=0,pos2=pos;
	f(t,0,T) flag_ans|=check(t,pos1,pos2);
	f(i,1,n) {
		if (pos1<pos&&dis2(p[pos1+1])==dis2(p2[i])) pos1++;
		else pos2++;
		f(t,0,T) flag_ans|=check(t,pos1,pos2);
		if (flag_ans) break;
	}
	f(i,1,n) p[i]=p1[i];
}

int main() {
	read(L);read(n);read(T);T/=2;
	f(i,1,n) read(p[i].x),read(p[i].y);
	sort(p+1,p+1+n,cmp2);
	f(i,1,n) p2[i]=p[i];
	sort(p+1,p+1+n,cmp1);
	f(i,1,n) p1[i]=p[i];
	f(i,0,n) solve(i);
	if (flag_ans) puts("Yes");
	else puts("No");
	return 0;
}

T2: Collecting Stamps 4

solution

签到题,切了。

先考虑固定一个起点。

考虑每种颜色,由于这种颜色第二次出现时前面的颜色种类数一定不少于第一次出现,所以这种颜色作为盖章卡右侧颜色最多能盖的盖章卡个数为其第二次出现位置前的颜色数,最终能盖的盖章卡个数即为每种颜色最多能盖的个数之和。

对于一次交换操作,交换了 \(i\) 和 \(i+1\) 位置,当 \(i\) 上的颜色为第二次出现且 \(i+1\) 位置颜色为第一次出现时,能使能盖的盖章卡数量增加 \(1\),不难发现当当前盖章卡个数不为 \(n^2\) 时必然存在一种交换方式能使盖章卡个数增加 \(1\)。

所以对于一个起点 \(i\),计算出初始能盖的盖章卡个数 \(w_i\),那么对于一个询问 \(k\),该起点的最小成本即为 \(c_i+\max(0,X\times (k-w_i))\)。

考虑快速求出每个起点对应的 \(w_i\),只需要考虑将第一个位置移到最后一个位置后盖章卡个数变化量为多少,破环为链,令第一个位置颜色出现位置为 \(L\),第二次出现位置为 \(R\),那么在 \(L,R)\\) 内的所有颜色贡献会减少 \\(1\\),即减去 \\(\[L,R)\\) 内的颜色数,再加上第一个位置颜色的变化量,即减去 \\(\[L,R\) 内颜色数并加上 \(n\),由于每种颜色的出现次数为 \(2\),所以用数据结构维护每种颜色第二次出现位置即可。

对于每个询问,答案为 \(\min\limits_{i=1}^{n\times 2}(c_i+\max(0,X\times (k-w_i)))\),对 \(w\) 排序并离线询问同时按照 \(k\) 排序,对于 \(k\le w_i\) 和 \(k>w_i\) 单独维护贡献,双指针即可。

时间复杂度 \(O(n\log n+q\log q)\)。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e6+10;
constexpr ll inf=(ll)(3e18)+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

inline void readl(ll &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m,X;
int a[N];
ll c[N],w[N];

struct Segment_Tree {
	int s[N<<2];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void update(int x,int l,int r,int now,int k) {
		if (l==r) return s[now]=k,void();
		int mid=l+r>>1;
		if (x<=mid) update(x,l,mid,ls(now),k);
		else update(x,mid+1,r,rs(now),k);
		s[now]=s[ls(now)]+s[rs(now)];
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (x<=l&&y>=r) return s[now];
		int mid=l+r>>1,res=0;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res+=query(x,y,mid+1,r,rs(now));
		return res;
	}
}tr;

inline void solve_w() {
	static int lst[N],L[N],R[N];
	ll sum=0,num=0;
	f(i,1,n) {
		int x=a[i];
		if (lst[x]) {
			tr.update(i,1,n*2,1,1);
			R[x]=i;sum+=num;
		}
		else L[x]=i,num++,lst[x]=1;
	}w[1]=sum;
	f(i,1,n-1) {
		int x=a[i];
		sum-=tr.query(L[x],R[x]-1,1,n*2,1);
		sum-=R[x]-1-tr.query(1,R[x]-1,1,n*2,1)-(i-1);
		tr.update(R[x],1,n*2,1,0);
		tr.update(i+n,1,n*2,1,1);
		L[x]=R[x];R[x]=i+n;sum+=n/2;
		w[i+1]=sum;
	}
}

ll ans[N],ed[N];
struct Node {ll c,w;}p[N],q[N];
inline bool cmp(Node x,Node y) {return x.w>y.w;}

int main() {
	read(n);read(X);n*=2;
	f(i,1,n) read(a[i]),a[i+n]=a[i];
	f(i,1,n) readl(c[i]),c[i+n]=c[i];
	solve_w();
	f(i,1,n) p[i]={c[i],w[i]};ed[n+1]=inf;
	sort(p+1,p+1+n,cmp);
	F(i,n,1) ed[i]=min(ed[i+1],p[i].c-X*p[i].w);
	read(m);
	f(i,1,m) readl(q[i].w),q[i].c=i;
	sort(q+1,q+1+m,cmp);
	int now=1;ll mn=inf;
	f(i,1,m) {
		while (now<=n&&q[i].w<=p[now].w) {
			mn=min(mn,p[now].c);
			now++;
		}
		ans[q[i].c]=min(mn,ed[now]+q[i].w*X);
	}
	f(i,1,m) printf("%lld\n",ans[i]);
	return 0;
}

T3: Space Thief

题意

给定一张 \(n\) 个点 \(m\) 条边的连通无向图,有一个隐藏的起点 \(s\) 和一个隐藏的终点 \(t\),每次询问你可以给边定向,并询问定向后 \(s\) 能否到达 \(t\),你需要在 \(70\) 次询问内确定 \(s\) 和 \(t\) 分别是哪个点,保证 \(s\ne t\)。

\(1\le n\le 10000,1\le m\le 15000\)。

solution

vp 时写了个强行点分治寻找,写了 \(8\) KB 并顶着 \(O(\log^2 n)\) 的次数和优秀的常数把树的测试点全过了获得了 \(80\) 的高分,赛后才知道复杂度是假的。

考虑将图定向为一个 DAG,若在这个 DAG 上 \(s\) 能够到达 \(t\),那么可以通过二分在 \(O(\log n)\) 的次数内确定 \(s\) 和 \(t\)。

具体地,考虑拓扑排序,对于一个区间 \(l,r\),若将两端点的拓扑序不都在 \(l,r\) 内的边全部反转,则 \(s\) 仍然可以到达 \(t\),当且仅当 \(s,t\in l,r\),因为 \(s\) 拓扑序在 \(t\) 之前,且 \(s\) 到 \(t\) 的路径经过点的拓扑序一定在它们拓扑序之间,所以反转后 \(s\) 到 \(t\) 的路径不受影响,又因为若 \(s\) 不在 \(l,r\) 中,则它指向拓扑序在它之后的点的边必然反转,此时 \(s\) 不再能够到达 \(t\),\(t\) 不在 \(l,r\) 中也同理,所以这个条件是充要的。

那么就可以根据拓扑序从小到大,二分一个最短前缀满足反转两端点的拓扑序不都在这段前缀内的边后 \(s\) 仍然可以到达 \(t\),那么 \(t\) 的拓扑序就是这段前缀的最后一个值,同理在这段前缀上二分一个后缀即可得到 \(s\)。

不难发现可以将问题从图上转化到树上,取图中一棵生成树,调整树边方向,由于树的定向一定是 DAG,而非树边只需要让拓扑序小的点指向大的点即可,这样一定能够定向成一张 DAG,所以只需要考虑树的情况。

比较常规的思路是点分治,然后对于子树进行二进制分组,以分治中心为根,一组全部设为内向树另一组设为外向树,若所有分组中均不存在 \(s\) 可以到达 \(t\) 的情况则 \(s\) 到 \(t\) 的路径一定不经过分治中心,将分治中心删去后对子树继续该过程即可,这么做每层二进制分组是 \(O(\log n)\) 的,总询问次数 \(O(\log^2 n)\),不可接受。

但是还有另一种点分治方式,令当前分治子树大小为 \(siz\),考虑将分治中心的若干棵子树分成两组,则一定存在一种分配方式,使得两组内子树大小之和均在 \(\\frac{1}{3} \\times siz,\\frac{2}{3} \\times siz\),按子树大小从小到大排序后,依次将子树加到一组中,直到这组子树大小超过 \(\frac{1}{3}\times siz\) 就停止,其余子树分到另一组即可。

根据这种分治方法,将分出的两组子树一组设为内向,一组设为外向,尝试是否存在 \(s\) 到 \(t\) 的路径,然后再将原先设为内向的组设为外向,另一组设为内向,再次尝试,若两次尝试都不能找到 \(s\) 到 \(t\) 的路径,则对这两组分别继续分治,分治中心在两组中同时保留,不能删除,由于 \(s\) 到 \(t\) 的路径一定不可能跨过这两个组别,所以路径一定在其中一组对应的连通块中,同时由于两个连通块只共点不共边,所以不会互相影响。

这样每层只需要进行两次询问,总次数是 \(O(\log n)\) 的,可以通过。
Code

cpp 复制代码
#include "thief.h"
#include<cstdio>
#include<algorithm>
#include<vector>
#include<map>
#include<bitset>
#include<queue>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;

int ans_st,ans_ed,_N;
vector<int> q;

int _U[N],_V[N];
map<int,int> mp[N];
vector<int> e[N];

inline void set_Edge(int u,int v) {
	if (!u||!v) return;
	int id=mp[u][v];
	if (u==_U[id]&&v==_V[id]) q[id]=0;
	else q[id]=1;
}

int in[N],dp[N],rk[N];
struct Edge{int v,id;};
vector<Edge> T[N];
bitset<N> did,dfn;
vector<int> G[N];

inline void topu() {
	queue<int> q;
	vector<int> id;
	f(i,1,_N) if (!in[i]) q.push(i);
	int idx=0;
	while (!q.empty()) {
		int now=q.front();
		dp[now]=++idx;
		rk[idx]=now;
		q.pop();
		for (int v:G[now]) {
			in[v]--;
			if (!in[v]) q.push(v);
		}
	}
	f(i,1,_N) in[i]=0,G[i].clear();
}

inline void solve() {
	int m=q.size();
	f(i,0,m-1) {
		int u=_U[i],v=_V[i];
		if (!q[i]) G[u].push_back(v),in[v]++;
		else G[v].push_back(u),in[u]++;
	}
	topu();
	int l=1,r=_N,res=-1;
	while (l<=r) {
		int mid=l+r>>1;
		f(i,0,m-1) {
			int u=_U[i],v=_V[i];
			if (dp[u]>mid||dp[v]>mid) q[i]^=1;
		}
		int ret=query(q);
		f(i,0,m-1) {
			int u=_U[i],v=_V[i];
			if (dp[u]>mid||dp[v]>mid) q[i]^=1;
		}
		if (ret) res=mid,r=mid-1;
		else l=mid+1;
	}
	ans_ed=rk[res];
	l=1,r=res-1;res=-1;
	while (l<=r) {
		int mid=l+r>>1;
		f(i,0,m-1) {
			int u=_U[i],v=_V[i];
			if (dp[u]<mid||dp[v]<mid) q[i]^=1;
		}
		int ret=query(q);
		f(i,0,m-1) {
			int u=_U[i],v=_V[i];
			if (dp[u]<mid||dp[v]<mid) q[i]^=1;
		}
		if (ret) res=mid,l=mid+1;
		else r=mid-1;
	}
	ans_st=rk[res];
}

inline int ask() {
	if (ans_st&&ans_ed) return 0;
	int m=q.size(),flag=0;
	f(i,0,m-1) {
		if (!did[i]) continue;
		int u=_U[i],v=_V[i];
		if (!q[i]) G[u].push_back(v),in[v]++;
		else G[v].push_back(u),in[u]++;
	}
	topu();
	f(i,0,m-1) {
		if (did[i]) continue;
		flag=1;
		int u=_U[i],v=_V[i];
		if (dp[u]<dp[v]) q[i]=0;
		else q[i]=1;
	}
	if (query(q)) return solve(),1;
	return 0;
}

bitset<N> vis;
int siz[N],wgh[N],rt,sum;

inline void get_rt(int now,int fa) {
	siz[now]=1;wgh[now]=0;
	for (int v:e[now]) {
		if (vis[v]||v==fa) continue;
		get_rt(v,now);
		siz[now]+=siz[v];
		wgh[now]=max(wgh[now],siz[v]);
	}
	wgh[now]=max(wgh[now],sum-siz[now]);
	if (wgh[now]<wgh[rt]) rt=now;
}

inline void add1(int now,int fa) {
	set_Edge(fa,now);
	for (int v:e[now]) if (v!=fa&&!vis[v]) add1(v,now);
}

inline void add2(int now,int fa) {
	set_Edge(now,fa);
	for (int v:e[now]) if (v!=fa&&!vis[v]) add2(v,now);
}

vector<int> _sub;
inline void get_sub(int now,int fa) {
	_sub.push_back(now);
	for (int v:e[now]) if (v!=fa&&!vis[v]) get_sub(v,now);
}

inline void solve(vector<vector<int>> tmp) {
	if (tmp.empty()) return;
	vector<vector<int>> nxt;
	for (vector<int> &vec:tmp) {
		rt=0;sum=vec.size();
		for (int x:vec) vis.reset(x);
		get_rt(vec.back(),0);
		int now=0,flag=0;
		vector<int> v1,v2,id;
		for (int v:e[rt]) if (!vis[v]) id.push_back(v);
		sort(id.begin(),id.end(),[](int x,int y){return siz[x]<siz[y];});
//		if (siz[id.back()]>sum/3) reverse(id.begin(),id.end());
		for (int v:id) {
			if (!flag) {
				now+=siz[v];add1(v,rt);
				get_sub(v,rt);
				for (int x:_sub) v1.push_back(x);
				_sub.clear();
				if (now>=sum/3) flag=1;
			}
			else {
				add2(v,rt);
				get_sub(v,rt);
				for (int x:_sub) v2.push_back(x);
				_sub.clear();
			}
		}
		for (int x:vec) vis.set(x);
		if (!v1.empty()) v1.push_back(rt),nxt.push_back(v1);
		if (!v2.empty()) v2.push_back(rt),nxt.push_back(v2);
	}
	if (ask()) return;
	for (int &x:q) x^=1;
	if (!ask()) solve(nxt);
}

inline void dfs_Tree(int now) {
	if (ans_st&&ans_ed) return;
	dfn.set(now);
	for (Edge E:T[now]) {
		int v=E.v,id=E.id;
		if (!dfn[v]) dfs_Tree(v),did.set(id);
	}
}

void solve(int N, int M, std::vector<int> U, std::vector<int> V) {
	vis.set();q.resize(M);_N=N;
	f(i,0,M-1) {
		U[i]++,V[i]++;
		_U[i]=U[i];_V[i]=V[i];
		T[U[i]].push_back({V[i],i});
		T[V[i]].push_back({U[i],i});
	}
	dfs_Tree(1);
	f(i,0,M-1) {
		if (!did[i]) continue;
		_U[i]=U[i];_V[i]=V[i];
		mp[U[i]][V[i]]=i,mp[V[i]][U[i]]=i;
		e[U[i]].push_back(V[i]);
		e[V[i]].push_back(U[i]);
	}
	wgh[0]=inf;
	vector<int> id;
	f(i,1,N) id.push_back(i);
	vector<vector<int>> st;
	st.push_back(id);solve(st);
	answer(ans_st-1,ans_ed-1);
}

Day3

\(59+19+100=178\)

三小时打完了能打的部分分,剩下两小时罚坐,赛后发现排名挺高的,好像没啥人会打其他的部分分。

T1: Bitaro the Brave 3

题意

有 \(N\) 只怪物,第 \(i\) 只怪物出现时间为 \(S_i\),强度 \(P_i\),初始生命值 \(H_i\)。

你在打防御战,共有 \(L\) 种等级,等级为 \(l\) 时怪物 \(i\) 生命值为 \(l\times H_i\),其余属性不变,防御战共持续 \(T\) 秒,每一秒你可以选择一个出现在场上且生命值不为 \(0\) 的怪物并对其攻击,使其生命值减 \(1\),令一局防御战后第 \(i\) 只怪物剩余血量为 \(h_i\),定义该局防御战的代价为 \(\sum\limits_{i=1}^{N}h_i\times P_i\)。

有 \(Q\) 次询问,每次询问给定一个阈值 \(M\),你需要设计攻击方案,使得代价不超过 \(M\) 的前提下,等级 \(l\) 最大,输出最大的 \(l\)(不存在则输出 \(0\)),保证每次询问 \(M\) 递增。

\(1\le N\le 6000,1\le L\le 10^7,1\le Q\le 10^6,1\le T\le 10^{18},1\le H_i,P_i,1\le \sum\limits_{i=1}^{N}H_i\times P_i \le 10^{11},0\le S_i<T\)。

solution

吓哭了呀。

考虑对每个 \(l\) 求出最小代价,每个询问双指针回答即可。

首先考虑将时间反转,即 \(S_i\leftarrow T-S_i\),这样就变成了全部怪物初始时出现,每只怪物在 \(S_i\) 时离开,这样更方便后面的推导。

我们规定 \(P_0=0,S_0=0\)。

首先考虑 \(P_i=1\) 时怎么做。

没了 \(P\) 的限制,我们可以直接对 \(S_i\) 排序,即优先处理截止时间较小的怪物,令 \(C_i=\sum\limits_{j=1}^{i} H_{j}\),那么可以得到最后的最小代价即为:

\f(l)=\\max(0,\\max_{i=1}\^{N}(C_i\\times l-S_i)) \\

答案下界显然是这个式子,因为前 \(i\) 只怪总血量 \(C_i\times l\),只有 \(S_i\) 的时间解决它们,所以至少剩余 \(C_i\times l-S_i\)。

证明一下答案上界是这个式子,设该最大值在 \(i=k\) 时取得。

  • 对于 \(j>k\),有 \(S_j-S_k\ge l\times (C_j-C_k)\),所以 \(j\) 一定会被打完(生命值归零);
  • 对于 \(j<k\),由于投入到前 \(k\) 只怪物上的时间是一段前缀,且长度为 \(\min(l\times C_k,\min\limits_{m=1}^{k}(S_m+l\times (C_k-C_m)))\),由于 \(S_m+l\times (C_k-C_m)\ge S_k\),所以长度为 \(\min(l\times C_k,S_k)\),若 \(l\times C_k\ge S_k\),则剩余 \(l\times C_k - S_k=f(l)\),符合最大值,否则说明前 \(k\) 只全部被打完,值为 \(0\)。

那么容易得到 \(f\) 是由若干条直线 \(y=C_i x-S_i\) 组成的上包络,由于 \(H_i>0\),有 \(C_i\) 递增,斜率递增即可 \(O(N)\) 使用单调栈求出这个包络,然后对包络上每条直线对其覆盖范围进行更新,维护斜率和截距的差分后 \(O(L)\) 前缀和即可求出,注意由于 \(l\) 只取整数值,交点需要进行上取整。

然后扩展到 \(P_i\ne 1\) 的情况,按 \(P\) 从小到大排序,将代价的式子写成另一种形式。

\\\sum_{j=1}\^{N} h_j\\times P_j=\\sum_{j=1}\^{N} \\sum_{P_i\\le P_j} h_j\\times (P_i-P_{i-1})= \\sum_{i=1}\^{N} (P_i-P_{i-1}) \\times \\sum_{P_j\\ge P_i} h_j \\

枚举 \(i\),后面的式子就与 \(P\) 无关了,将所有 \(P_j\ge P_i\) 的 \(j\) 拿出来跑一遍 \(P=1\) 的情况,将当前层包络对答案的贡献乘上系数 \(P_{i}-P_{i-1}\),对所有 \(i\) 做完后再做前缀和即可得到答案。

不过 \(h_j\) 根据最终策略决定,但是枚举 \(i\) 每次单独计算得到的 \(h_j\) 不一定相同,这样上面的做法还正确吗?

可以证明一定存在一组 \(h_j\) 使得每轮 \(i\) 计算时,代价恰好达到 \(f_i(l)\)(该层最小代价)。

根据贪心,攻击怪物的方案应该是根据 \(P\) 从大到小,依次考虑每只怪物,并且对于一只怪物 \(i\),从 \(S_i\) 向前选取最晚未占用的 \(H_i\times l\) 的时间攻击它,根据这样的策略,从 \(f_i(l)\) 到 \(f_{i-1}(l)\) 时,第 \(j(j\ge i)\) 只怪物的攻击方案是已经确定的,所以 \(h_{j}\) 不会因为加入了 \(i-1\) 这只怪物而改变,同时,这组 \(h_j\) 就是根据最优贪心确定的结果,一定符合 \(f_i(l)\) 和 \(f_{i-1}(l)\) 的最优性,所以上面的做法是正确的。

总时间复杂度 \(O(N^2+L+Q)\)。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=6e3+10;
constexpr int M=1e7+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

inline void read(ll &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

struct Line{ll k,b;}d[M];
inline ll Line_corss(Line x,Line y) {
	ll u=y.b-x.b,d=x.k-y.k;
	if (d<0) u=-u,d=-d;
	return u/d+(u%d!=0);
}

int n,m,Len;
ll T,ans[M];
struct Node{ll s,h,p;}a[N];
inline bool cmp(Node x,Node y) {return x.s<y.s;}

inline void solve(ll lim,ll v) {
	if (!v) return;
	static Line st[N];
	int top=0;ll sum=0;
	st[++top]={0,0};
	f(i,1,n) {
		if (a[i].p<lim) continue;
		sum+=a[i].h;
		Line now={sum,-a[i].s};
		while (top>1&&Line_corss(st[top-1],st[top])>=Line_corss(st[top],now)) top--;
		st[++top]=now;
	}
	ll k=0,b=0;
	f(i,2,top) {
		ll x=Line_corss(st[i],st[i-1]);
		if (x>m) break;
		d[x].k+=(st[i].k-k)*v;
		d[x].b+=(st[i].b-b)*v;
		k=st[i].k,b=st[i].b;
	}
}

int main() {
	read(n);read(m);read(T);
	f(i,1,n) read(a[i].s),read(a[i].h),read(a[i].p),a[i].s=T-a[i].s;
	sort(a+1,a+1+n,cmp);
	static ll mp[N],Len;
	f(i,1,n) mp[++Len]=a[i].p;
	sort(mp+1,mp+1+Len);
	Len=unique(mp+1,mp+1+Len)-mp-1;
	f(i,1,Len) solve(mp[i],mp[i]-mp[i-1]);
	ll k=0,b=0;
	f(i,1,m) {
		k+=d[i].k,b+=d[i].b;
		ans[i]=k*i+b;
	}
	int q,now=0;read(q);
	while (q--) {
		ll d;read(d);
		while (now<m&&ans[now+1]<=d) now++;
		printf("%d\n",now);
	}
	return 0;
}

T2: Conference

题意

给定一个长度为 \(n\),仅包含 \(\texttt{A}\)、\(\texttt{B}\)、\(\texttt{C}\) 和 \(\texttt{?}\) 且满足首尾均为 \(\texttt{A}\) 的字符串 \(S\),共有 \(q\) 次询问,每次询问给出三个参数 \(X,Y,Z\),你需要求出将 \(S\) 中 \(X\) 个 \(\texttt{?}\) 替换为 \(\texttt{A}\),\(Y\) 个 \(\texttt{?}\) 替换为 \(\texttt{B}\),\(Z\) 个 \(\texttt{?}\) 替换为 \(\texttt{C}\)(保证 \(X+Y+Z\) 恰为 \(S\) 中 \(\texttt{?}\) 的数量),使得满足 \(S_i \ne S_{i+1}(1\le i<N)\) 的下标 \(i\) 数量最小,求该数量的最小可能值,询问间独立。

\(2\le n\le 3\times 10^5,1\le q\le 2\times 10^5\)。

solution

难飞了。

对于每个询问,令 \(d_A=X,d_B=Y,d_C=Z,M=X+Y+Z\)。

对于一段连续的 \(\texttt{?}\) 进行考虑,考虑其两个端点,只有端点字母相同和不同两种情况,记为 \(X\) 型和 \(XY\) 型,分别表示两端均为字符 \(X\) 的段和一端为 \(X\),另一端为 \(Y\) 的段。

首先一些位置必然对答案造成贡献,这个贡献为 \(S_{i}\ne \texttt{?} \land S_{i+1} \ne \texttt{?} \land S_{i}\ne S_{i+1}\) 的数量加上 \(XY\) 型段的数量,这些贡献无论如何都不可能被消去,所以直接计入答案,那么每次询问需要考虑的是除去必要贡献后的增加量。

先不考虑替换数量的限制,考虑如何才能最小化 \(S_{i}\ne S_{i+1}\) 数量,显然对于 \(X\) 型段,中间的问号全部替换为 \(X\),对于 \(XY\) 型段,一段前缀替换为 \(X\),一段后缀替换为 \(Y\),这样的填法使得额外贡献为 \(0\)。

先明确一些基本事实:对于 \(X\) 型段,在其中填入两种字符(有一种字符为 \(X\))会造成 \(2\) 的额外贡献,填入三种字符会造成 \(3\) 的额外贡献,对于 \(XY\) 型段,在其中填入三种字符会造成 \(1\) 的额外贡献(一段前缀填 \(X\),一段后缀填 \(Y\),中间填 \(Z\))。

令 \(P(X)\) 为 \(X\) 型段长度总和,\(P(XY)\) 为 \(XY\) 型段长度总和。

对于每个字符 \(X\),可以求出最优情况(额外贡献为 \(0\))下至少填入数量 \(L_X=P(X)\) 和最多填入数量 \(R_X=P(X)+\sum\limits_{Y\ne X}P(XY)\),那么一组 \((d_A,d_B,d_C)\) 能够取到最优情况,当且仅当对于任意字符 \(X\) 均有 \(L_X\le d_X\le R_X\)。

首先若 \(d_X\) 不在 \(L_X,R_X\) 中,则调整方案必然使得额外贡献增加,若 \(d_X\) 在 \(L_X,R_X\) 内,构造一个二分图,左部点为各种字符,右部点为每个 \(\texttt{?}\) 连续段(容量为段长),字符 \(X\) 只能匹配到 \(X\) 型段或 \(XY\) 型段上,要求字符 \(X\) 恰好匹配 \(d_X\) 的单位容量,则根据 Hall 定理,对于单字符子集就是上面的上界限制,对于两个字符子集 \(X,Y\),另一个字符为 \(Z\),有限制 \(d_X+d_Y\le M- L_Z\),那么移项后恰为 \(L_Z\le d_Z\),即为下界限制,三个字符的子集是平凡的。

如果某个 \(d_X\) 不在 \(L_X,R_X\) 中,我们需要对方案进行调整,于是根据 \(d_X>R_X\) 进行分类讨论,由于 \(\sum R_X\ge M = \sum d_X\),所以满足 \(d_X>R_X\) 的字符 \(X\) 的数量不超过两个,令这个数量为 \(num\)。

num=0

所有字符均满足上界限制,但是可能有字符不满足下界限制,并且最多只可能有一个字符不满足下界,否则可以推导出第三个字母会不满足上界,令不满足下界的字符为 \(X\),则有 \(d_X<L_X\),由于 \(L_X=P(X)\),所以对于这种情况的唯一处理方案就是在左右端点均为 \(X\) 的 \(\texttt{?}\) 连续段中填入其他字符,每修改一个段额外贡献会加 \(2\),需要修改的段长总量为 \(L_X-d_X\),根据长度从大到小替换即可,利用前缀和配合二分可以做到 \(O(\log n)\) 回答一次询问。

num=1

钦定不满足上界限制的字符为 \(A\),那么我们需要至少让 \(R_A\) 增加 \(d_A-R_A\)。

首先 \(B,C\) 可能不满足下界条件,若一个字符 \(X\) 不满足下界限制则像 \(num=0\) 时的处理一样按长度从大到小依次将 \(A\) 填入 \(X\) 型段中,\(R_A\) 增加量即为段长,这样做显然是不劣的。

若处理完下界后仍有 \(R_A<d_A\),那么共有三种替换方法(含有 \(A\) 的段并不能使 \(R_A\) 增加):

  • 在 \(B\) 型段中间放入 \(A\),代价加 \(2\);
  • 在 \(C\) 型段中间放入 \(A\),代价加 \(2\);
  • 在 \(BC\) 型段中间放入 \(A\),代价加 \(1\)。

这三种方法能给 \(R_A\) 带来的增加量均为其段长,这不是我们的 sale 吗,贪心地根据性价比(段长与代价增加量之比)从大到小选择物品(即为此题中的段),直到 \(R_A\) 不小于 \(d_A\),这样贪心得到的代价最多比最优代价多 \(1\)。

由于将用性价比更低且代价为 \(2\) 的物品替换当前选择的物品一定不优,所以得到代价比最优代价多 \(1\) 的情况只有以下两种:

  • 去掉前面某一个代价为 \(1\) 的物品,\(R_A\) 仍然可以不小于 \(d_A\);
  • 用后面某一个代价为 \(1\) 的物品替换当前选择的一个代价为 \(2\) 的物品,\(R_A\) 仍然可以不小于 \(d_A\)。

于是我们可以使用线段树进行维护,根据性价比排序后选择一段前缀使得 \(R_A\ge d_A\),这段前缀可以二分出来,然后考虑选中代价为 \(1\) 和 \(2\) 的段长最小值分别为多少,以及未选中的代价为 \(1\) 的段长最大值,可以二分求出来或者在线段树上直接维护,单组询问可以做到 \(O(\log n)\) 回答。

注意在处理下界时用过的段不能作为一个物品参与后续的选择,由于用过的段是按段长从大到小排序后的一段前缀,所以维护起来并不困难。

num=2

钦定满足上界限制的字符为 \(A\),那么需要让 \(R_B\) 增加 \(cap_B=d_B-R_B\),\(R_C\) 增加 \(cap_C=d_C-R_C\),那么有:

\cap_B+cap_C=d_B+d_C-(P(B)+P(C)+P(AB)+P(AC)+2\\times P(BC)) \\

即:

\cap_B+cap_C=(M-d_A)-(M-P(A)+P(BC))=L_A-d_A-P(BC) \\

由于 \(cap_B+cap_C+P(BC)>0\),所以可以得到 \(d_A<L_A\),即字符 \(A\) 必然不满足下界条件,并且 \(L_A-d_A\ge cap_B+cap_C\),所以通过调整 \(A\) 的下界,可以做到将 \(B\) 和 \(C\) 的上界调整至合法。

调整下界的方法和之前一样,依次选择最长的段进行填入其他字符,令调整的段数为 \(k\),那么额外贡献下界为 \(2\times k\)。

从小到大考虑需要调整的段,先填入字符 \(B\),直到 \(d_B-R_B\) 不超过段长,在当前段调整完 \(B\) 后立刻调整 \(C\),剩下的段全部用于调整 \(C\),那么只有一段含有三种字符,所以额外贡献上界为 \(2\times k+1\)。

额外贡献达到下界,要求没有任何一个调整段填入了三种字符,所以现在问题变成了:对于所有调整段,令段长和为 \(T\),是否能够选出一个调整段的集合,使得其段长和 \(w\) 满足 \(cap_B\le w\le T-cap_C\),即让这个集合内的调整段填入字符 \(B\),其余段填入字符 \(C\),使得 \(B\) 和 \(C\) 的上界均满足条件。

这个问题看起来只能用背包解决,朴素的 bitset 优化背包可以做到 \(O(\frac{n^2}{\omega })\),但是无法通过。

考虑单调队列优化多重背包,令 \(f_i\) 为选出一个段长和为 \(i\) 的子集至少需要多少个段(即最短前缀的段数),对于一个段长 \(v\),段数为 \(c\),令段长大于 \(v\) 的段数为 \(sum\),那么有转移 \(f_{i}= \min\limits_{\frac{(i-j)}{v}\le c\land j<i\land i\equiv j \pmod{v}}(f_i,\max(f_j,sum+\frac{(i-j)}{v}))\),每次转移只需要考虑之前没有被覆盖过的 \(f_i\) 即可,由于若 \(j\) 可以被 \(v\) 之前的前缀表示出来,则必然有 \(f_j<sum+\frac{(i-j)}{v}\),所以可以根据 \(\bmod v\) 的值分成若干个转移链,在每条链上从小到大更新,并维护之前最大能够表示出来的 \(j\) 进行转移即可,这样对于每个段长 \(v\) 需要 \(O(n)\) 时间复杂度就可以完成转移。

由于段长和至多为 \(n\),所以不同的段长数量为 \(O(\sqrt n)\),整个转移就是 \(O(n\sqrt n)\) 的。

对于每个字符预处理出对应的 \(f\) 值,并做 RMQ(或线段树),每次询问 \(cap_B,T-cap_C\) 内 \(f\) 最小值,判断其是否不超过 \(k\) 即可知道额外贡献是否可以达到下界,单组询问 \(O(\log n)\)。

于是这个题就做完了,总时间复杂度 \(O(n\sqrt n+q\log n)\)。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=3e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n;
char ch[N];
int a[N],L[4],R[4];
vector<int> vec[4][4],svec[4];

inline int find(vector<int> &tmp,int x) {
	int l=0,r=tmp.size()-1,res=0;
	while (l<=r) {
		int mid=l+r>>1;
		if (tmp[mid]>=x) res=mid,r=mid-1;
		else l=mid+1;
	}
	return res+1;
}

inline int solve1(int x,int y,int z) {
	int ret=0;
	if (x<L[1]) ret+=find(svec[1],L[1]-x);
	if (y<L[2]) ret+=find(svec[2],L[2]-y);
	if (z<L[3]) ret+=find(svec[3],L[3]-z);
	return ret<<1;
}

struct Node {int v,w,id;};
inline bool cmp(Node x,Node y) {return x.v*2/x.w>y.v*2/y.w;}

struct Segment_Tree1 {
	int Len,c1,c2;
	int mn1[N<<2],mx1[N<<2],mn2[N<<2],s[N<<2],t[N<<2];
	int R1[N<<2],R2[N<<2],s1[N<<2],t1[N<<2],s2[N<<2],t2[N<<2];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void build(int l,int r,int now,vector<Node> &tmp) {
		if (l==r) {
			Node k=tmp[l-1];
			if (k.w==1) mx1[now]=mn1[now]=k.v,mn2[now]=inf;
			else mn2[now]=k.v,mn1[now]=inf;
			s[now]=k.v;t[now]=k.w;
			if (k.id==1) {
				R1[now]=++c1;
				s1[now]=s[now];
				t1[now]=t[now];
			}
			if (k.id==2) {
				R2[now]=++c2;
				s2[now]=s[now];
				t2[now]=t[now];
			}
			return;
		}
		int mid=l+r>>1;
		build(l,mid,ls(now),tmp);
		build(mid+1,r,rs(now),tmp);
		mn1[now]=min(mn1[ls(now)],mn1[rs(now)]);
		mx1[now]=max(mx1[ls(now)],mx1[rs(now)]);
		mn2[now]=min(mn2[ls(now)],mn2[rs(now)]);
		s[now]=s[ls(now)]+s[rs(now)];
		t[now]=t[ls(now)]+t[rs(now)];
		R1[now]=max(R1[ls(now)],R1[rs(now)]);
		R2[now]=max(R2[ls(now)],R2[rs(now)]);
		s1[now]=s1[ls(now)]+s1[rs(now)];
		s2[now]=s2[ls(now)]+s2[rs(now)];
		t1[now]=t1[ls(now)]+t1[rs(now)];
		t2[now]=t2[ls(now)]+t2[rs(now)];
	}
	
	inline void init(vector<Node> &tmp) {
		if(tmp.empty())return;
		Len=tmp.size();
		build(1,Len,1,tmp);
	}
	
	inline int query(int l,int r,int now,int k,int MN1,int MN2,int MX1,int ret,int p1,int p2,int lst_s1,int lst_s2,int lst_t1,int lst_t2) {
		if (l==r) {
			MN1=min(MN1,mn1[now]);
			MN2=min(MN2,mn2[now]);
			k-=s[now];ret+=t[now];
			if (k+MN1<=0) return ret-1;
			if (k+MN2-MX1<=0) return ret-1;
			return ret;
		}
		int mid=l+r>>1,val_s=s[ls(now)],val_t=t[ls(now)];
		if (R1[ls(now)]<=p1) val_s-=s1[ls(now)],val_t-=t1[ls(now)];
		else val_s-=lst_s1,val_t-=lst_t1;
		if (R2[ls(now)]<=p2) val_s-=s2[ls(now)],val_t-=t2[ls(now)];
		else val_s-=lst_s2,val_t-=lst_t2;
		if (val_s>=k) return query(l,mid,ls(now),k,MN1,MN2,max(MX1,mx1[rs(now)]),ret,p1,p2,min(lst_s1,s1[ls(now)]),min(lst_s2,s2[ls(now)]),min(lst_t1,t1[ls(now)]),min(lst_t2,t2[ls(now)]));
		return query(mid+1,r,rs(now),k-val_s,min(MN1,mn1[ls(now)]),min(MN2,mn2[ls(now)]),MX1,ret+val_t,p1,p2,max(0,lst_s1-s1[ls(now)]),max(0,lst_s2-s2[ls(now)]),max(0,lst_t1-t1[ls(now)]),max(0,lst_t2-t2[ls(now)]));
	}
}tr1[4];

inline int solve2(int x,int y,int z) {
	int ret=0,cnt=0,pos1=0,pos2=0,pos3=0,s1=0,s2=0,s3=0;
	if (x<L[1]) {
		pos1=find(svec[1],L[1]-x);
		ret+=pos1*2,cnt+=(s1=svec[1][pos1-1]);
	}
	if (y<L[2]) {
		pos2=find(svec[2],L[2]-y);
		ret+=pos2*2,cnt+=(s2=svec[2][pos2-1]);
	}
	if (z<L[3]) {
		pos3=find(svec[3],L[3]-z);
		ret+=pos3*2,cnt+=(s3=svec[3][pos3-1]);
	}
	if (x>R[1]) {
		x-=cnt;if (x<=R[1]) return ret;
		return ret+tr1[1].query(1,tr1[1].Len,1,x-R[1],inf,inf,0,0,pos2,pos3,s2,s3,pos2*2,pos3*2);
	}
	if (y>R[2]) {
		y-=cnt;if (y<=R[2]) return ret;
		return ret+tr1[2].query(1,tr1[2].Len,1,y-R[2],inf,inf,0,0,pos1,pos3,s1,s3,pos1*2,pos3*2);
	}
	if (z>R[3]) {
		z-=cnt;if (z<=R[3]) return ret;
		return ret+tr1[3].query(1,tr1[3].Len,1,z-R[3],inf,inf,0,0,pos1,pos2,s1,s2,pos1*2,pos2*2);
	}
	puts("Error_solve2");
}

struct Segment_Tree2 {
	int mn[N<<2],dp[N];
	inline int ls(int x) {return x<<1;}
	inline int rs(int x) {return x<<1|1;}
	
	inline void build(int l,int r,int now) {
		if (l==r) return mn[now]=dp[l],void();
		int mid=l+r>>1;
		build(l,mid,ls(now));
		build(mid+1,r,rs(now));
		mn[now]=min(mn[ls(now)],mn[rs(now)]);
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (x<=l&&y>=r) return mn[now];
		int mid=l+r>>1,res=inf;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res=min(res,query(x,y,mid+1,r,rs(now)));
		return res;
	}
	
	int t[N],sum;
	inline void init_dp(vector<int> &tmp) {
		for (int x:tmp) t[x]++;
		f(i,1,n) dp[i]=inf;
		F(i,n,1) {
			if (!t[i]) continue;
			const int cnt=t[i],Len=i-1;
			f(c,0,Len) {
				int lst=-1;
				for (int j=c;j<=n;j+=i) {
					if (dp[j]!=inf) lst=j;
					if (lst!=-1&&dp[j]==inf&&(j-lst)/i<=cnt) dp[j]=sum+(j-lst)/i;
				}
			}
			sum+=t[i];
		}
		build(0,n,1);
	}
}tr2[4];

inline int solve3(int x,int y,int z) {
	int op=((x<L[1])?1:((y<L[2])?2:3)),c1=0,c2=0;
	int k=find(svec[op],max({L[1]-x,L[2]-y,L[3]-z})),sum=svec[op][k-1];
	if (op==1) c1=y-R[2],c2=z-R[3];
	if (op==2) c1=x-R[1],c2=z-R[3];
	if (op==3) c1=x-R[1],c2=y-R[2];
	if (tr2[op].query(c1,sum-c2,0,n,1)<=k) return k<<1;
	return k<<1|1;
}

inline void init() {
	f(i,1,3) {
		for (int x:vec[i][i]) svec[i].push_back(x);
		for (int k=1,L=svec[i].size();k<L;k++) svec[i][k]+=svec[i][k-1];
		tr2[i].init_dp(vec[i][i]);
	}
	f(i,1,3) {
		vector<Node> tmp;int now=0;
		f(j,1,3) if (i^j) {
			now++;
			for (int x:vec[j][j]) tmp.push_back({x,2,now});
			f(k,j+1,3) if (k^i) for (int x:vec[j][k]) tmp.push_back({x,1,0});
		}
		sort(tmp.begin(),tmp.end(),cmp);
		tr1[i].init(tmp);
	}
	f(i,1,3) f(j,i,3) {
		if (i==j) {for (int x:vec[i][j]) L[i]+=x,R[i]+=x;}
		else {for (int x:vec[i][j]) R[i]+=x,R[j]+=x;}
	}
}

int main() {
	read(n);
	scanf("%s",ch+1);
	f(i,1,n) a[i]=(ch[i]=='?'?0:ch[i]-'A'+1);
	int num=0,lst=0,sum=0;
	f(i,1,n) {
		sum+=((i!=n)&&(a[i]!=0)&&(a[i+1]!=0)&&(a[i]!=a[i+1]));
		if (!a[i]) {
			if (!num) lst=a[i-1];
			num++;
		}
		else if (num) {
			vec[min(lst,a[i])][max(lst,a[i])].push_back(num);
			num=0;
		}
	}
	f(i,1,3) f(j,i,3) {
		sort(vec[i][j].begin(),vec[i][j].end(),[](int x,int y){return x>y;});
		sum+=(i!=j)*vec[i][j].size();
	}
	int q;read(q);init();
	while (q--) {
		int x,y,z;read(x);read(y);read(z);
		int num=(x>R[1])+(y>R[2])+(z>R[3]);
		if (num==0) printf("%d\n",sum+solve1(x,y,z));
		if (num==1) printf("%d\n",sum+solve2(x,y,z));
		if (num==2) printf("%d\n",sum+solve3(x,y,z));
	}
	return 0;
}

T3: Multi Communication

题意

这是一道提交答案题。

有 \(N\) 个人在进行游戏,初始时,裁判秘密指定其中一个人为"目标"(除了目标本身外,其他人不知道目标是谁)。

每人有一块白板。游戏共进行 \(L\) 个回合,每回合:

  1. 每人根据自己已知的信息,在白板上写下符号 T 或 F。
  2. 每人选择一个参与者,查看其白板上的符号。

\(L\) 个回合结束后,所有人都必须正确猜出目标是谁。

要求设计一个策略,使得无论目标是谁,所有人最终都能正确猜出,且回合数 \(L\) 尽可能小。

你需要输出:对于每个人作为初始选中的目标,给出每个参与者在每回合写下的符号和选择查看的对象。

\(N=4,32,48\),满分要求分别为 \(L\le 2,8,9\)。

solution

好玩,感觉是这几场签到题中最有意思的。

以下用 \(1\) 和 \(0\) 代表 \(T\) 和 \(F\)。

N=4

让目标一直在自己的白板上写 \(1\),其余人写 \(0\),每个人需要观察两个互不相同,且都不是自己的人,这样每个人就能够确定三个人是否为目标,从而推出剩下一个人是否为目标。

N=32

\(32=2^6\),看起来具有比较优秀的性质,考虑一个分治结构,对于当前分治区间 \(l,r(l\ne r)\),令 \(mid=\left\lfloor \frac{l+r}{2} \right\rfloor\),分为 \(l,mid\) 和 \(mid+1,r\) 两部分,我们想让 \(l,r\) 内的所有人都知道目标在 \(l,mid\) 中还是 \(mid+1,r\) 中,或者目标不在 \(l,r\) 中,首先递归两个区间,于是 \(l\) 知道了目标是否位于 \(l,mid\) 中,\(r\) 知道了目标是否位于 \(mid+1,r\) 中,若目标在 \(l,mid\) 中,则让 \(l\) 在白板上写 \(1\),否则写 \(0\),若目标在 \(mid+1,r\) 中,则让 \(r\) 在白板上写 \(1\),否则写 \(0\),\(l+1,r-1\) 内的全部人都写 \(0\),对于 \(l,mid\) 内所有人,让他们查看 \(r\) 白板上的符号,根据其确定目标是否在 \(mid+1,r\) 中,同理 \(mid+1,r\) 内所有人根据 \(l\) 白板上的内容判断目标是否在 \(l,mid\) 中。

对整体进行一次分治后,每个人都知道目标位于某个长度为 \(2^k(0\le k\le 4)\) 的区间内,再根据这个分治结构,从上到下进行二分,即设此时每个人都已知目标在 \(l,r\) 中,令 \(mid=\left\lfloor \frac{l+r}{2} \right\rfloor\),那么根据初始的分治结构,\(l\) 一定知道目标是否在 \(l,mid\) 中,若目标在 \(l,mid\) 中,让 \(l\) 在白板上写 \(1\),否则写 \(0\),然后让所有人查看 \(l\) 白板上的内容,就能够判断出目标在 \(l,mid\) 中还是 \(mid+1,r\) 中,继续该过程即可。

分治层数看似是 \(6\),实际上最下面一层的每个区间只有一个人,不需要考虑,而最上面一层(\(1,32\))也不用考虑,因为第一次分治时只需要通过第二层(长度为 \(16\) 的层)往后的过程,就能让 \(1,16\) 内的人知道目标是否在 \(1,16\) 内,若不在则一定在 \(17,32\) 内,不需要观察编号为 \(32\) 的人白板上的内容来确定,对于 \(17,32\) 内的人也同理,所以实际有意义的分治层数为 \(4\),两个阶段只需要 \(4+4=8\) 个回合就能让所有人确定目标。

N=48

和 \(N=32\) 类似,注意到 \(48=3\times 2^4\)。

于是分成三个区间 \(1,16,17,32,33,48\) 进行上面的第一次分治,这样每个人都知道了目标是否在自己所在区间,然后在目标所在区间的左端点白板上写上 \(1\),其余写 \(0\),每个人观察另外两个区间中某一个区间的左端点,那么每个人就知道了目标是否在其他一个区间内,从而推出目标是否在剩下的一个区间内,于是每个人都知道了目标在这三个区间中的哪一个,再使用与 \(N=32\) 相同的二分即可确定目标,需要回合数为 \(4+1+4=9\)。

Day4

\(66+100+15=181\)

欸欸欸怎么调不出来啊,欸欸欸怎么就结束了呜呜呜。

T1: Circuit 2

solution

欸欸欸怎么调不出来啊,诶诶怎么没时间了啊,\(66\) 分遗憾离场,距离满分一步之遥。

不难发现开关形成了一个二叉树结构,叶子即开关 \(N\sim 2N\),根为开关 \(0\),令 \(v_i\) 表示开关 \(i\) 是否开启,一个叶子节点 \(i\) 返回值即为 \(v_i\),非叶子节点 \(i\) 的返回值即为其左右子节点返回值经过组件槽 \(i\) 运算后再异或上 \(v_i\) 得到的值,最终的返回值即为根节点返回值。

我们希望做的事情是:将某些节点作为待测节点,若这些节点中存在组件为 \(\texttt{or}\) 的节点则让根节点返回 \(1\),否则返回 \(0\)。

Subtask \(5\)

仅考虑非叶子节点,它们构成了一条链,对于一段前缀 \(0,i\),将 \(j\in0,i\) 的节点 \(j\) 右子节点(即输入的 \(V_j\))设为开启,其他节点全部关闭,发现若 \(0,i\) 中有一个组件槽为 \(\texttt{or}\) 则根节点返回 \(1\),否则返回 \(0\),于是一个显然的思路就是二分,每次找到第一个组件槽为 \(\texttt{or}\) 的节点 \(j\),并在后面设置前缀时将其右子节点设为关闭即可,需要次数约为 \(R\log_2 N\),可以通过 Subtask \(4\)。

考虑优化一下,直接二分范围太大了,所以不妨考虑分块,每块块长为 \(B\),每次检查一个块内有没有尚未找到的组件槽为 \(\texttt{or}\) 的节点,若有则在块内二分找到其中一个并标记,直到块内所有 \(\texttt{or}\) 均被找到,这样需要的次数约为 \(\frac{N}{B}+R(\log_2 B+1)\),取 \(B=32\) 可以通过,注意检查一个块时需要将上方的开关设置成只用于传递信号的状态(不会自己产生信号,但下面的信号可以传至根节点)。

Subtask \(7\)

这是一棵完全二叉树,发现它的深度很低,不妨从深度下手。

我们发现如果用 \(1\) 作为有 \(\texttt{or}\) 出现的信号则组件槽为 \(\texttt{and}\) 的节点难以传递子节点的信号,于是考虑设计新的信号,若当前组件槽为 \(\texttt{and}\),则若一个子节点传入 \(0\) 信号说明当前有 \(\texttt{or}\) 出现,若当前节点组件槽为 \(\texttt{or}\),则若一个子节点传入 \(1\) 信号说明当前有 \(\texttt{or}\) 出现,由于对组件 \(\texttt{and}\) 传入 \(0\) 必然返回 \(0\),所以信号不会因为另一个子树传入的信息而消失,\(\texttt{or}\) 同理,根据父节点需要的信号来设置当前节点的 \(v\) 值,不难发现这样能够顺利地将信号传递至根节点。

由于同一层的子树两两不交,不会相互影响测试,且测试时需要父节点传递信息,所以考虑由浅到深逐层确定其中的组件槽 \(\texttt{or}\),对于一个非叶子节点,将其左子树内叶子节点全设为关闭,右子树叶子节点全设为开启,子树内非叶子节点(除了这个节点本身)全部设为关闭,若这个节点组件槽为 \(\texttt{or}\) 则会得到 \(1\) 的信号,否则会得到 \(0\) 的信号,浅于这层的开关要设置成只用于传递信号的状态,根据父节点的需要设置当前节点的 \(v\) 值即可将这个信号传递至根节点。

然后和 Subtask \(5\) 一样对当前层节点分块做二分来找到使根输出变为 \(1\) 的(即组件为 \(\texttt{or}\) 的)节点,次数约为 \(\frac{N}{B}+R(\log_2 B+1)+\log_2 N\),取 \(B=32\) 可以通过。

正解

其实打完以上两个 Subtask 就已经很接近正解了。

找到当前最长的一条自上而下由组件尚未确定的节点组成的链,根据其长度进行讨论。

  • 若这条链长度不小于 \(B\),则用 Subtask \(5\) 的方法处理掉这条链,区别在于上面的是对右边节点设为开启,这里是对链上节点的链外子树设为开启;
  • 若这条链长度小于 \(B\),则使用 Subtask \(7\) 的方法扩展,由于这是最长的未确认的链,所以扩展的次数不会超过 \(B\),同时最长链长度不增,所以后面一定一直使用这种扩展方式。

总次数约为 \(\frac{N}{B}+R(\log_2 B+1)+B\),取 \(B=32\) 可以通过,取 \(B=64\) 次数更少。
Code

cpp 复制代码
#include "circuit.h"
#include<cstdio>
#include<algorithm>
#include<string>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=16000+10;
constexpr int B=64;

int n;
int v[N],ls[N],rs[N],f[N];
int vis[N],ans[N],mxd[N];

//int query(std::string s);
inline int ask() {
	string s;
	f(i,0,n*2) s+=(v[i]+'0');
	return query(s);
}

inline void fill_col(int now,int c) {
	if (!ls[now]&&!rs[now]) return v[now]=c,void();
	v[now]=0;
	fill_col(ls[now],c);
	fill_col(rs[now],c);
}

inline void dfs_fa(int now) {
	if (!ls[now]&&!rs[now]) return;
	f[ls[now]]=f[rs[now]]=now;
	dfs_fa(ls[now]);
	dfs_fa(rs[now]);
}

inline void setin(int L,int R,vector<int> &tmp) {
	f(i,L,R) if (!ans[tmp[i]]) {
		fill_col(ls[tmp[i]],0);
		fill_col(rs[tmp[i]],1);
		v[tmp[i]]=ans[f[tmp[i]]]^1;
	}
}

inline void reset(int L,int R,vector<int> &tmp) {
	f(i,L,R) {
		int now_col=v[tmp[i]];
		fill_col(tmp[i],1^v[tmp[i]]^ans[f[tmp[i]]]);
		v[tmp[i]]=now_col;
	}
}

inline void solve_dep(vector<int> tmp) {
	reset(0,tmp.size()-1,tmp);
	for (int L=0,mxLen=tmp.size()-1,R=min(L+B-1,mxLen);L<=mxLen;L+=B,R=min(L+B-1,mxLen)) {
		setin(L,R,tmp);
		int flag=ask(),lst=L;
		reset(L,R,tmp);
		while (flag) {
			int l=lst,r=R,res=-1;
			while (l<=r) {
				int mid=l+r>>1;
				setin(l,mid,tmp);
				int ret=ask();
				reset(l,mid,tmp);
				if (ret) res=mid,r=mid-1;
				else l=mid+1;
			}
			ans[tmp[res]]=1;
			lst=res+1;
			setin(L,R,tmp);
			flag=ask();
			reset(L,R,tmp);
		}
	}
	for (int x:tmp) v[x]=ans[x]^ans[f[x]],vis[x]=1;
	reset(0,tmp.size()-1,tmp);
}

inline void solve_len(vector<int> tmp) {
	fill_col(tmp.front(),0);
	for (int L=0,mxLen=tmp.size()-1,R=min(L+B-1,mxLen);L<=mxLen;L+=B,R=min(L+B-1,mxLen)) {
		v[tmp[L]]=ans[f[tmp[L]]]^1;
		f(i,L,R) fill_col(rs[tmp[i]],1);
		int flag=ask(),lst=L;
		f(i,L,R) fill_col(rs[tmp[i]],0);
		while (flag) {
			int l=lst,r=R,res=-1;
			while (l<=r) {
				int mid=l+r>>1;
				f(i,L,mid) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],1);
				int ret=ask();
				f(i,L,mid) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],0);
				if (ret) res=mid,r=mid-1;
				else l=mid+1;
			}
			ans[tmp[res]]=1;
			lst=res+1;
			f(i,L,R) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],1);
			flag=ask();
			f(i,L,R) if (!ans[tmp[i]]) fill_col(rs[tmp[i]],0);
		}
		f(i,L,R) {
			v[tmp[i]]=ans[tmp[i]]^ans[f[tmp[i]]];
			v[rs[tmp[i]]]=ans[tmp[i]]^1;
			vis[tmp[i]]=1;
		}
	}
	fill_col(ls[tmp.back()],ans[tmp.back()]^1);
}

vector<int> tmp;
inline void dfs1(int now) {
	if (!ls[now]&&!rs[now]) return;
	if (!vis[now]) {
		tmp.push_back(now);
		return;
	}
	dfs1(ls[now]);dfs1(rs[now]);
}

inline void dfs2(int now) {
	mxd[now]=1;
	if (!ls[now]&&!rs[now]) return;
	dfs2(ls[now]);dfs2(rs[now]);
	mxd[now]=max(mxd[ls[now]],mxd[rs[now]])+1;
}

inline void dfs3(int now) {
	if (!ls[now]&&!rs[now]) return;
	tmp.push_back(now);
	if (mxd[ls[now]]<mxd[rs[now]]) swap(ls[now],rs[now]);
	dfs3(ls[now]);
}

inline bool solve_all() {
	tmp.clear();dfs1(0);
	if (tmp.empty()) return 0;
	vector<int> now=tmp,mx;
	for (int x:now) {
		tmp.clear();
		dfs2(x);dfs3(x);
		if (tmp.size()>mx.size()) mx=tmp;
	}
	if (mx.size()>=B) solve_len(mx);
	else solve_dep(now);
	return 1;
}

std::string solve(int _N, int R, std::vector<int> U, std::vector<int> V) {
	n=_N;
	f(i,0,n-1) ls[i]=U[i],rs[i]=V[i];
	fill_col(ls[0],0);
	fill_col(rs[0],1);
	ans[0]=ask();v[0]=ans[0]^1;vis[0]=1;
	fill_col(ls[0],ans[0]^1);
	fill_col(rs[0],ans[0]^1);
	dfs_fa(0);
	while (solve_all());
	string ret;
	f(i,0,n-1) ret+=(ans[i]?'|':'&');
	return ret;
}

T2: Migration Plan

solution

最签到的一题。

由于操作 \(1\) 是对整个层整体操作,所以考虑直接维护每层节点的值,合并起来看起来可以均摊复杂度,不过将子树中有值的位置移到父亲没值的位置复杂度就假了,所以考虑每层开一棵线段树维护 dfn 序从而确定一个节点子树合并上来的权值,那么线段树合并即可。

操作二是平凡的在线段树上单点加,操作三则是在求 dfn 在子树内的和。

时间复杂度 \(O(n\log n)\)。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e6+10;
constexpr int P=6e7;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m,dfn_idx,Tree_tot;
int f[N],a[N],rt[N];
int dep[N],siz[N],dfn[N];
vector<int> e[N];

struct Segment_Tree {
	struct Tree_Node {int ls,rs,sum;}tr[P];
	#define ls(x) tr[x].ls
	#define rs(x) tr[x].rs
	
	inline void update(int &now,int x,int l,int r,int k) {
		if (!now) now=++Tree_tot;
		if (l==r) return tr[now].sum+=k,void();
		int mid=l+r>>1;
		if (x<=mid) update(ls(now),x,l,mid,k);
		else update(rs(now),x,mid+1,r,k);
		tr[now].sum=tr[ls(now)].sum+tr[rs(now)].sum;
	}
	
	inline int merge(int x,int y,int l,int r) {
		if (!x||!y) return x|y;
		if (l==r) return tr[x].sum+=tr[y].sum,x;
		int mid=l+r>>1;
		ls(x)=merge(ls(x),ls(y),l,mid);
		rs(x)=merge(rs(x),rs(y),mid+1,r);
		tr[x].sum=tr[ls(x)].sum+tr[rs(x)].sum;
		return x;
	}
	
	inline int query(int x,int y,int l,int r,int now) {
		if (!now) return 0;
		if (x<=l&&y>=r) return tr[now].sum;
		int mid=l+r>>1,res=0;
		if (x<=mid) res=query(x,y,l,mid,ls(now));
		if (y>mid) res+=query(x,y,mid+1,r,rs(now));
		return res;
	}
}tr;

inline void dfs(int now) {
	siz[now]=1;
	dfn[now]=++dfn_idx;
	tr.update(rt[dep[now]],dfn[now],1,n,a[now]);
	for (int v:e[now]) {
		dep[v]=dep[now]+1;
		dfs(v);
		siz[now]+=siz[v];
	}
}

int main() {
	read(n);
	f(i,2,n) read(f[i]),e[f[i]].push_back(i);
	f(i,1,n) read(a[i]);
	dfs(1);read(m);
	while (m--) {
		int op,x,y;
		read(op);read(x);
		if (op==1) {
			read(y);
			rt[y]=tr.merge(rt[x],rt[y],1,n);
			rt[x]=0;
		}
		if (op==2) {
			read(y);
			tr.update(rt[dep[x]],dfn[x],1,n,y);
		}
		if (op==3) printf("%d\n",tr.query(dfn[x],dfn[x]+siz[x]-1,1,n,rt[dep[x]]));
	}
	return 0;
}

T3: Uiro

题意

给定长度为 \(N\) 的序列 \(A\),有 \(Q\) 次询问,每次询问给定 \(L,R\),初始时有一个数字 \(v=0\) 和一个计数器 \(c=0\),你需要依次考虑 \(A_L,A_{L+1},A_{L+2},\dots,A_{R}\),令当前考虑的数为 \(y\),你可以选择以下两种操作中的一种:

  • \(v\leftarrow v-y,c\leftarrow c+1\);
  • \(v\leftarrow v+y\),\(c\) 不变。

你需要在保证 \(v\) 始终非负的前提下,最大化最后得到 \(c\) 的值,输出这个最大值。

\(1\le N,Q\le 2\times 10^5,1\le A_i\le 100\)。

solution

感觉比 T1 难啊。

下文中选中位置 \(i\) 表示考虑 \(A_i\) 时使用了操作,\(v\leftarrow v-A_i,c\leftarrow c+1\)。

首先将每个询问的区间看作一个新的数组单独考虑,我们需要发现一些性质:

  • 对于两个位置 \(i,j\),若 \(i<j\) 且 \(A_i>A_j\),那么一定不可能选中了位置 \(i\) 而未选中位置 \(j\),否则选中位置 \(j\) 不选位置 \(i\) 一定不劣;
  • 对于一种值 \(x\),一定存在一个分界点 \(p\),使得对于 \(A_i=x\) 的位置,若 \(i< p\) 则不选择 \(i\) 否则选择 \(i\),若不满足则有 \(i<j\) 且 \(A_i=x,A_j=x\) 满足 \(i\) 被选择而 \(j\) 没被选择,这种选法一定不优于选择 \(j\) 而不选择 \(i\);
  • 对于两种值 \(x,y\),若 \(x<y\),则最后一个不被选择的值为 \(x\) 的位置一定在第一个选择的值为 \(y\) 的位置之前,否则选择 \(x\) 最后一个原来未被选择位置而不选第一个原来选择 \(y\) 的位置一定不劣。

根据这几条性质,我们可以得到一个做法的框架:首先钦定所有位置均不选择,从小到大考虑每个值 \(x\),对于 \(A_i=x\) 的位置,选择最长的后缀使得选择后可行,答案即为最终选择的位置总数,可行指的是小于 \(x\) 的位置按照原先选择,等于 \(x\) 的位置按当前后缀选择,大于 \(x\) 的位置全部不选择,根据此策略有 \(v\) 恒非负,根据调整法可以证明是正确的。

将询问离线下来,枚举 \(x\),钦定值小于 \(x\) 的位置 \(i\) 贡献为 \(-A_i\),值大于等于 \(x\) 的位置 \(i\) 贡献为 \(A_i\),令 \(S_i\) 表示前 \(i\) 个位置的贡献和,\(num_i\) 表示前 \(i\) 个位置有多少个位置值为 \(x\),\(pos_i\) 表示第 \(i\) 个值为 \(x\) 的位置,对于每个询问,维护:\(lst\) 表示第一个选中的值为 \(x\) 的位置必须在 \(lst\) 之后(满足第三条性质的限制),\(sum\) 表示未被选中的值小于 \(x\) 的位置权值和的两倍,\(ans\) 表示值小于 \(x\) 的选中的位置个数。

对于一个询问 \((l,r)\),二分分界点,令第 \(pos_{mid}\) 及其之后的值为 \(x\) 的位置都需要被选中(需满足 \(lst\le pos_{mid}\le r\)),令 \(h(p)\) 为考虑到第 \(p\) 个位置的 \(v\) 值,那么对于 \(pos_{mid}\le p\le r\),有 \(h(p)=S_p-S_{l-1}+sum-2\times x\times (num_p-mid+1)\),因为值小于 \(x\) 且未被选中的位置都在 \(lst\) 之前,而判定区间从 \(pos_{mid}\ge lst\) 开始,所以对区间内任意 \(p\),\(sum\) 中的修正已全额生效,可以直接作为常数,对于 \(l\le p<pos_{mid}\),由于此轮选中的位置在 \(pos_{mid}\) 之后,所以 \(h(p)\) 的值和上一轮得到的值相同,而由于上一轮可行,所以一定有 \(h(p)\ge 0\),那么只需要满足对于\(pos_{mid}\le p\le r\) 有 \(h(p)\ge 0\) 即可。

由于 \(h(p)\ge 0\) 等价于 \(S_p-2\times x\times num_p\ge S_{l-1}-sum-2\times x\times (mid-1)\),所以令 \(f(p)=S_p-2\times x\times num_p\),二分出 \(mid\) 后判定条件变为 \(\min\limits_{p=pos_{mid}}^{r} f(p)\ge S_{l-1}-sum-2\times x\times (mid-1)\),由于 \(mid\) 增大区间变短,左边递增,右边递减,满足单调性,所以可以二分,对 \(f\) 建立 ST 表进行查询即可做到 \(O(1)\) 判定。

二分出分界点后,令第一个合法的 \(mid\) 为 \(res\),无合法 \(mid\) 时 \(res=num_r+1\),更新询问对应的 \(lst\leftarrow \max(lst,pos_{res-1}+1)\),\(sum\leftarrow sum+2\times x\times (res-1-num_{l-1})\),\(ans\leftarrow ans+num_r-res+1\),每个询问每层时间复杂度为 \(O(\log n)\),每层预处理出各种信息以及建出 ST 表的时间复杂度为 \(O(N\log N)\)。

令值域为 \(V\),那么总时间复杂度就是 \(O((N+Q)V\log N)\),精细实现可以做到 \(O(N\log N+QV\log N)\)。

讲讲如何精细实现,由于二分时查询左端点值一定为 \(x\),建 ST 表时实际上维护两个相邻的值为 \(x\) 的位置及其之间 \(f\) 值的最小值,再对于两个相邻的值为 \(x\) 的位置之间的 \(f\) 值做前缀 \(\min\),那么查询只需要查区间包含的所有值为 \(x\) 位置及其之间 \(f\) 值的最小值,和一段不包含值为 \(x\) 的位置的前缀即可,那么建表的长度和为 \(N\),建 ST 表的总时间复杂度为 \(O(N\log N)\)。
Code

cpp 复制代码
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m;
int a[N];
struct ques{int l,r,lst,sum,ans;}q[N];

struct Sparse_Table {
	int Len;
	int a[N],lg[N],mx[20][N];
	
	inline void init() {f(i,2,n) lg[i]=lg[i>>1]+1;}
	inline void push_back(int x) {a[++Len]=x;mx[0][Len]=x;}
	inline void clear() {Len=0;}
	inline void build() {F(i,Len,1) f(j,1,lg[Len-i+1]) mx[j][i]=min(mx[j-1][i],mx[j-1][i+(1<<(j-1))]);}
	
	inline int query(int l,int r) {
		if (l>r) return inf;
		int k=lg[r-l+1];
		return min(mx[k][l],mx[k][r-(1<<k)+1]);
	}
}ST;

inline void solve(int x) {
	static int s[N],num[N],pos[N],f[N];
	int cnt=0;f[0]=inf;
	f(i,1,n) {
		s[i]=s[i-1]+((a[i]<x)?-a[i]:a[i]);
		num[i]=num[i-1];
		if (a[i]==x) num[i]++,pos[++cnt]=i;
		f[i]=s[i]-2*x*num[i];
		if (a[i]!=x) f[i]=min(f[i],f[i-1]);
	}
	ST.clear();
	f(i,1,cnt-1) {
		int L=pos[i],R=pos[i+1],mn=inf;
		ST.push_back(min({f[L],f[R],f[R-1]}));
	}
	ST.build();
	f(i,1,m) {
		const int l=q[i].l,r=q[i].r,add=q[i].sum;
		int L=num[q[i].lst-1]+1,R=num[r],res=-1;
		while (L<=R) {
			int mid=L+R>>1,now=s[l-1]-add-2*x*(mid-1);
			if (min(ST.query(mid,num[r]-1),f[r])>=now) R=mid-1,res=mid;
			else L=mid+1;
		}
		res=(res==-1)?num[r]+1:res;
		q[i].ans+=num[r]-res+1;
		q[i].lst=max(q[i].lst,pos[res-1]+1);
		q[i].sum+=(res-1-num[l-1])*x*2;
	}
}

int main() {
	read(n);ST.init();
	f(i,1,n) read(a[i]);
	read(m);
	f(i,1,m) read(q[i].l),read(q[i].r),q[i].lst=q[i].l;
	f(i,1,100) solve(i);
	f(i,1,m) printf("%d\n",q[i].ans);
	return 0;
}