跌倒的地方

P7116 [NOIP2020] 微信步数

首先我们可以想到,如果说多次路线中没有任意一次走出场地,我们实际上可以将一次完整路线看成一大步。然后我们同时可以处理出一次行走路线距离出发点向 \(k\) 个方向最多走多远。然后我们已知其在哪一次行走路线中出去的话,就直接求出前缀各个方向最远距离二分即可。

这样我们就可以做到在 \(O(\log)\) 的时间下处理出每一个点的方法,这能拿到 \(40pts\)

然后我们再来考虑一波怎么快速的求出所有的点。

哦,我们考虑到不同的出界位置实际上是 \(O(n)\) 的,不同指的是在路线的不同位置出去。我们实际上可以扫一遍路线,然后求出从这个位置出去的边界有多少,应该是可以 \(O(k)\) 计算的。

然后需要考虑内部的点实际上内部的点最终还是会走到边界点的,而走的方式就是我们前面说的大步走。边界点实际上就是这个 \(k\) 维矩形的外层,剩下部分应该还是一个 \(k\) 维矩形,我们应该是可以直接算的。

首先由于大步走的方向和长度我们是确定的,所以实际上就是我们将整个内部点矩形向同一个方向移动,再将走到边界点的部分删去,加上对应的贡献。这个部分如何快速计算呢?

哦,这道题目有一个关键就是我们考虑计算出每一步骤的剩余点的时候每一个维度是可以分开计算的。我们实际上只需要统计每一时刻存在点数量求和就是每一个点的步数了。

我们就考虑暴力枚举当前轮数 \(x\) 和当前步数 \(i\) ,剩下的每一个维度在当前步数的剩余点都是一个关于 \(x\) 的一次函数,其中有一个常量 \(f_i\) 的意义在当前步数的情况下,当前维度在非第一轮会新扩张的点数,直接暴力走两轮就可以求出了。我们就直接考虑暴力将 \(m\) 个维度的这个关于 \(x\) 的多项式直接乘起来。

然后实际上是要我们求出这个多项式的 \([1,T]\) 的点值,其中 \(T\) 是当前步数的最大合法轮数,这个我们暴力 \(O(m)\) 求。然后我们考虑调换枚举顺序,利用 \(\sum_{k=1}^m\sum_{x=1}^Tx^k\) 来计算这个多项式,其中 \(\sum_{x=1}^Tx^k\) 是可以预处理的,这个做法的复杂度是 \(O(nm^2)\)

#include<bits/stdc++.h>
using namespace std;

const int N=5e5+5,W=1e6+5,K=11;
const int INF=1e9+7;
const int MOD=1e9+7;

int ADD(int x,int y){if((x+=y)>=MOD)return x-MOD;return x;}
int SUB(int x,int y){if((x-=y)<0)return x+MOD;return x;}
int TIME(int x,int y){return (int)(1ll*x*y%MOD);}
int ksm(int x,int k=MOD-2){int res=1;for(;k;k>>=1,x=TIME(x,x))if(k&1)res=TIME(res,x);return res;}

bool flag=false;

int n,k,c[N],d[N];
int w[K],L[K],R[K];
int f[K],del[K][N],res=1;

struct Poly{
	vector<int> f;
	int &operator [] (int x){return f[x];}
	int size(){return f.size();}
};
Poly operator * (Poly a,Poly b){
	Poly res;
	res.f.resize(a.size()+b.size()-1);
	for(int i=0;i<a.size();++i) for(int j=0;j<b.size();++j)
		res[i+j]=ADD(res[i+j],TIME(a[i],b[j]));
	while(res.size()>1&&!res.f.back()) res.f.pop_back();
	return res;
}

int h[K][W],tag[K];

int solve(int n,int k){
	if(k==0) return n+1;
	else if(k==1) return TIME(TIME(n,n+1),ksm(2));
	else if(k==2) return TIME(TIME(ADD(n,n+1),TIME(n,n+1)),ksm(6));
	else if(k==3) return TIME(solve(n,1),solve(n,1));
	else{
		if(tag[k]) return h[k][n];
		h[k][0]=(k==0),tag[k]=true;
		for(int i=1;i<W;++i) h[k][i]=ADD(h[k][i-1],ksm(i,k));
		return h[k][n];
	}
}

int main(){
	freopen("walk.in","r",stdin);
	freopen("walk.out","w",stdout);

	cin>>n>>k;
	for(int i=1;i<=k;++i) scanf("%d",&w[i]);
	for(int i=1;i<=n;++i) scanf("%d%d",&c[i],&d[i]);

	for(int i=1;i<=k;++i) L[i]=1,R[i]=w[i];
	for(int i=1;i<=k;++i) res=TIME(res,w[i]);

	for(int i=1;i<=n;++i){
		L[c[i]]=max(1,L[c[i]]+d[i]);
		R[c[i]]=min(w[c[i]],R[c[i]]+d[i]);
		if(L[c[i]]>R[c[i]]){flag=true;break;}

		int sum=1;
		for(int j=1;j<=k;++j) sum=TIME(sum,R[j]-L[j]+1);
		res=ADD(res,sum);
	}

	if(flag) return printf("%d\n",res),0;

	for(int i=1;i<=k;++i) f[i]=R[i]-L[i]+1;

	for(int i=1;i<=n;++i){
		for(int j=1;j<=k;++j) del[j][i]=del[j][i-1]+(R[j]-L[j]+1);
		if(L[c[i]]<=R[c[i]]){
			L[c[i]]=max(1,L[c[i]]+d[i]);
			R[c[i]]=min(w[c[i]],R[c[i]]+d[i]);
		}
		for(int j=1;j<=k;++j) del[j][i]=del[j][i]-(R[j]-L[j]+1);
	}

	for(int i=1;i<=n;++i){
		int T=INF;Poly g={{1}};
		for(int j=1;j<=k;++j) if(del[j][n]) T=min(T,(f[j]-del[j][i])/del[j][n]);
		if(T==INF) return printf("-1\n"),0;
		for(int j=1;j<=k;++j) g=g*(Poly){{SUB(f[j],del[j][i]),SUB(0,del[j][n])}};
		for(int j=0;j<=k;++j) res=ADD(res,TIME(solve(T,j),g[j]));
	}

	printf("%d\n",res);
	
	return 0;
}
/*
想不到吧,我又来了插值题。不会捏。
先求出走不满一轮就出来的点的个数。大体就是每一个维度的晃动范围,可以求出一次走出的点。
我们还可以尝试求出每一步出去的点的和,关键是后面的点。

我们可以明确的是,每次对于这个超立方体,我们可以划出一个外表皮,所有进入这个外表皮的点都会在当前轮出去,并且步数是
确定的。
而且可以确定的是,每次少掉的部分的在每一个维度的边长都是一样的,为位移的距离,除非是最后一次。

感觉这是一个 k 次左右的函数,所以我们暴力求出。
-----------------------------------------------------------------------------------------------
哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈,我还是做不出来啊,之前明明都搞懂了一遍了,作用是让我这一次看题解懂得快一点是吗?
除去第一次后,每一次的消去个数都是相同的,我们暴力处理出在第一次后的每轮每一个维度丧失掉的个数,一次在一轮中该位
丧失掉的个数,然后我们相当于是要求出每一个时刻存活的点的个数。
任何一个时刻任何一个维度的数量都可以用上面的东西求出来。
然后考虑以轮数为 x 求出这个多项式的各项系数,利用自然数幂和求出这个式子的值。

注:压根不是插值题捏。
*/

P7115 [NOIP2020] 移球游戏

发现自己一年之后还是没什么想法。

考虑到 \(n\le 50\)\(m\le 400\)\(opt\le 820000\) ,我们毛估估一下应该是 \(O(n^2m)\) 的复杂度?

考虑到对于构造题褚学长说的,我们可以将其转化成一些有意义的基本操作,然后用这个基本操作来完成题目。

我能想到的基本操作就是将任意一个点与另一堆的堆顶交换,换句话说,交换两个深度和小于 \(m\) 的点,这个操作的复杂度是 \(2m\)

于是我得到了一个想法,将柱子分成上下两部分,对于上半部分,任意两个点都可以在 \(m\) 次操作以内交换。我们考虑单纯利用交换来还原?存在一定的问题,即我们不能保证上半部分的颜色是恰好满足上半部分的。

我们还发现了一种操作,\(reverse\) 一个堆,其复杂度也是 \(m\) 的,这样的话,也就是说我们可以在 \(4m\) 次操作内交换任意两个位置。

于是我们应该就得到了一个巨大暴力的做法,复杂度应该是 \(O(nm^2)\) 的,卡卡常应该可以得到 \(40pts\)

然后我便一点想法没有了。


我们考虑一个一个颜色枚举过去。此时我们就使得等于该颜色的置为 \(1\) ,否则置为 \(0\) ,我们的目标是换出一个全 \(1\) 的列。

我们考虑如果我们存在一个全 \(0\) 列,实际上我们就可以利用这个全 \(0\) 列来搞出全 \(1\) 列,我们首先根据我们的颜色选择出一个存在 \(1\) 的列,将这个全 \(0\) 列分成两堆,然后使得存在 \(1\) 的那一列恰好可以分成全 \(1\) 和全 \(0\) 的两个部分。这个时候我们再找到一列存放我们的 \(1\) ,也是利用类似的方法。

然后如何找到一个全 \(0\) 列呢?可知,我们任意选择两列,其中 \(0\) 的个数必然大于等于 \(n\) ,然后我们先利用第二列,将第一列的 \(0\)\(1\) 分开,然后利用第一列,调整一下 \(0\)\(1\) 的数量,再将第二列的 \(0\)\(1\) 分开,这样就可以得到全 \(0\) 列了。

这样我们实际上就可以在 \(O(nm)\) 的次数下复原一个颜色。考虑到常数实际上是带一个 \(\frac{1}{2}\) 的,所以我们再卡卡常应该就行了。

应该就做完了。


然后好像应该还有一个做法,我们考虑两个满列和一个空列,其中正好是两种颜色各 \(m\) 个。我们很容易就将两种颜色分开。

我们考虑这个做法如何拓展?

我们考虑实际上可以将整个区间分成左右两边,其中颜色在左边的设为 \(1\) ,颜色在右边的设为 \(0\)

#include<bits/stdc++.h>
using namespace std;
const int N=55,M=4e2+5;
int n,m;
vector<int> bag[N];
vector<pair<int,int> > res;
void print(){
	for(int i=1;i<=n+1;++i){
		printf("%d: ",i);
		for(int j=0;j<(int)bag[i].size();++j)
		printf("%d ",bag[i][j]);
		printf("\n");
	}
}
void ed(){
	printf("%d\n",(int)res.size());
	for(int i=0;i<(int)res.size();++i)
		printf("%d %d\n",res[i].first,res[i].second);
}
void move(int x,int y){
	assert(!bag[x].empty());
	assert((int)bag[y].size()<m);
	res.push_back(make_pair(x,y));
	bag[y].push_back(bag[x].back()),bag[x].pop_back();
}
int count(int x,int col){
	int cnt=0;
	for(int i=0;i<m;++i) cnt+=(bag[x][i]<=col);
	return cnt;
}
void opt1(int x,int y,int col){
	int tmp=count(x,col);
	for(int i=0;i<tmp;++i) move(y,n+1);
	for(int i=m-1;i>=0;--i){
		if(bag[x][i]<=col) move(x,y);
		else move(x,n+1);
	}
	for(int i=0;i<tmp;++i) move(y,x);
	for(int i=tmp;i<m;++i) move(n+1,x);
	for(int i=tmp;i<m;++i) move(y,n+1);
	for(int i=tmp;i<m;++i) move(x,y);
	for(int i=m-1,j=m-tmp;i>=0;--i){
		if(bag[n+1][i]<=col&&j) move(n+1,x),j--;
		else move(n+1,y);
	}
}
void opt2(int x,int y,int col){
	int tmp=m-count(y,col);
	for(int i=0;i<tmp;++i) move(x,n+1);
	for(int i=m-1;i>=0;--i){
		if(bag[y][i]>col) move(y,x);
		else move(y,n+1);
	}
	for(int i=0;i<tmp;++i) move(x,y);
	for(int i=tmp;i<m;++i) move(n+1,y);
	for(int i=tmp;i<m;++i) move(x,n+1);
	for(int i=tmp;i<m;++i) move(y,x);
	for(int i=m-1,j=m-tmp;i>=0;--i){
		if(bag[n+1][i]>col&&j) move(n+1,y),j--;
		else move(n+1,x);
	}
}
void work(int l,int r){
	if(l==r) return ;
	int mid=(l+r)>>1;
	for(int i=l,j=r;i<j;){
		int sum=count(i,mid)+count(j,mid);
		if(sum>=m) opt1(i,j,mid),i++;
		else opt2(i,j,mid),j--;
		// printf("%d %d\n",i,j),print();
	}
	work(l,mid),work(mid+1,r);
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;++i){
		for(int j=1,x;j<=m;++j)
		scanf("%d",&x),bag[i].push_back(x);
	}
	work(1,n);
	return ed(),0;
}
/*
5 5
1 3 1 4 2
4 3 2 2 2
1 5 5 4 3
3 3 5 5 1
2 4 4 1 5
*/

[PKUSC2021] 逛街

给定序列 \(\{a_n\}\)\(q\) 次操作,类型如下:

  1. 给定 \(l,r\) ,从小到大枚举 \(i\)\(l\)\(r-1\) ,修改 \(a_i\)\(\max\{a_i,a_{i+1}\}\)
  2. 给定 \(l,r\) ,贪心地求 \([l,r]\) 中严格上升子序列的元素之和(即如果当前元素严格大于当前上升子序列的末尾,就把它加入上升子序列)。

题解

感觉自己完全不会做。哎,感觉自己在做这些题的时候总会会想到当时的那个窒息的情景。

我们冷静一下思考,你考虑到如果只是回答第二个问题,实际上我们可以比较轻易地想到每一个点通过连向其向右的第一个比其大的点来,然后第二个问题就转化成了维护该点到根的权值和,求 \(\text{lca}\) ,然后权值和相减即可。

这样的话实际上我们只需要处理出在操作一的情况下,这棵树的编号变化和形态变化即可。不会处理啊,感觉好难。

哦,我们可以将维护序号和我们一开始的那棵树分离开来。

我们考虑维护一个数组 \(b_i\) ,表示\(a_i'=\max\{a_i,a_{i+1},\cdots,a_{b_i}\}\) ,然后我们每一次操作一实际上就是一个单点添加或删除,这个用平衡树就可以维护了?

也就是说我们每一次询问的时候都能够找到 \(a_l'\)\(a_r'\) 的位置,然后求一个 \(\text{lca}\) 就行了?哦,不对,中间的位置还是可能被修改的,这里我们处理得随便了。

我们需要考虑将消失的点删去,即消去其权值的贡献。我们考虑我们删去的节点有两种,叶子节点和非叶子节点。对于叶子节点,实际上我们是不需要多考虑的,因为我们从这里出发的点会因为 \(b_i\) 的维护而不会在这里出发,经过这里的点如果经过了这里,证明其子树还存在,说明其不会因为作为叶子而被删掉。对于非叶子节点,他如果被删除了一定只会出现在修改操作的开头,所以我们直接找到这个点删除即可。

然后就做完了。

[PKUSC2021] 代金券

\(n\) 个菜品,每个菜品价格为 \(v_i\) 元。【】要依次品尝这 \(n\) 个菜品。若每次购买的菜品价格为 \(a\) , 她可以使用 \(b\) \((0\le b\le a)\) 张代金券,每张代金券相当于 \(1\) 元。也就是他用 \(b\) 张代金券后仍需支付 \(a−b\) 元。同时,他可以获得 \(\lfloor\frac{a−b}{c}\rfloor\) 张代金券,其中 \(c\) 是给定的正整数。

【】最开始一张代金券也没有。食堂的菜品价格有 \(q\) 次变动。对于每次变动,食堂会更改一个菜品的价格。每一次变动后【】都想知道他最少要花多少钱,才能依次品尝 \(n\) 个菜品。

题解

对于这种题目,我们必然需要先考虑单次询问怎么做。不 wei 捉。

考虑这样的一个贪心,就是我们令 \(d_i=a_i\bmod c\) ,那么对于这一个菜品,我们使用 \(d_i\) 个代金券必然是不劣的。但是我们这样使用是可能多出代金券的,我们需要在一段后缀中尽量多地使用代金券。嗯,感觉这个后缀尽量多地使用代金券中还是有很多细节的。

哇,读了题解,感觉这题好叼啊,我怎么这么菜啊。

首先明确这个后缀的定义,即一分钱不花全部只用代金券。

那么必然会存在一个点,我们即不能全部用代金券购买,又不能只使用 \(a_i\bmod c\) 的代金券。

即点 \(i\) 表示 \([i,n]\) 不可以全部使用代金券购买,\([i+1,n]\) 可以全部使用代金券购买。

我们试图找到这个点,这意味着我们需要维护前缀能获得最大的代金券数量。我们考虑到代金券是怎么搞的,首先我们先使用 \(x=\max(x-a_i\bmod c,0)\) ,然后再获得 \(x=x+\lfloor\frac{a_i}{c}\rfloor\) 。两者是可以合并的,即,

\[x=\max(x-a_i\bmod c+\lfloor\frac{a_i}{c}\rfloor,\lfloor\frac{a_i}{c}\rfloor)\\ \]

我们考虑到实际上这个东西是可以快速维护的,我们将一次运算看成 \(x=\max(x+a,b)\) ,实际上这个东西是可以合并的。

\[x=\max(\max(x+a_1,b_1)+a_2,b_2)\\ =\max(x+a_1+a_2,\max(b_1+a_2,b_2))\\ \]

这个我们直接用线段树维护即可。然后我们在线段树上二分就可以找到这个 \(i\) 的位置。同时对于 \(i\) 的使用代金券数量,是可以二分找到的。于是我们就做完了。

感觉不是很难啊,是自己被吓到了。哎。

P7078 [CSP-S2020] 贪吃蛇

存在一个结论:

当前最强的蛇吃了最弱的蛇之后,如果没有变成最弱的蛇,他一定会选择吃。

证明可以分成两种情况:

  • 最强的蛇吃了还是最强的蛇,相当于白吃一次,不吃白不吃。
  • 最强的蛇吃了不是最强的蛇,那么此时的最强就是原来的次强,此时的最弱就是原来的次弱,此时的最强如果选择吃了,那么其产生的那只蛇必然比原最强产生的那只蛇小,而前者一定会保证自己的安全,所以后者一定能安全,所以吃。

那如果吃完之后变最小了就一定不能吃吗?不是的,我们考虑一下几种情况:

  • 吃完之后只剩一条蛇了,干嘛不吃。
  • 吃完之后的下一条蛇要保证自己的安全,也可以吃。

我们考虑此时哪些蛇会保证自己的安全。我们发现必然是连续成为最小值的若干个最大值中,位于奇数位置的。

然后由于我们发现的单调性,直接处理即可。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int T,n,cnt=0;
struct Snake{int data,id;}a[N];
bool operator < (Snake a,Snake b){return a.data==b.data?a.id<b.id:a.data<b.data;}
bool operator > (Snake a,Snake b){return a.data==b.data?a.id>b.id:a.data>b.data;}
Snake operator - (Snake a,Snake b){Snake res=a;res.data-=b.data;return res;}
deque<Snake> q1,q2;
int solve(){
	cnt=0;
	while(!q1.empty()) q1.pop_back();
	while(!q2.empty()) q2.pop_back();
	for(int i=1;i<=n;++i) q1.push_front(a[i]);
	while(true){
		Snake Min=q1.back(),Max;q1.pop_back();
		if(q1.empty()&&q2.empty()) break;
		if(!q1.empty()&&!q2.empty()){
			if(q1.front()>q2.front()) Max=q1.front(),q1.pop_front();
			else Max=q2.front(),q2.pop_front();
		}
		else if(!q1.empty()) Max=q1.front(),q1.pop_front();
		else Max=q2.front(),q2.pop_front();
		if(q1.empty()||Max-Min<q1.back()){
			bool flag=true;
			while(true){
				if(q1.empty()&&q2.empty()) break;
				if(!q1.empty()&&Max-Min>q1.back()) break;
				if(!q2.empty()&&Max-Min>q2.back()) break;
				Min=Max-Min,flag^=1;
				if(!q1.empty()&&!q2.empty()){
					if(q1.front()>q2.front()) Max=q1.front(),q1.pop_front();
					else Max=q2.front(),q2.pop_front();
				}
				else if(!q1.empty()) Max=q1.front(),q1.pop_front();
				else Max=q2.front(),q2.pop_front();
			}
			cnt+=flag;break;
		}
		else q2.push_back(Max-Min),cnt++;
	}
	return n-cnt;
}
int main(){
	cin>>T>>n,T--;
	for(int i=1;i<=n;++i)
		scanf("%d",&a[i].data),a[i].id=i;
	printf("%d\n",solve());
	while(T--){
		int k;cin>>k;
		for(int i=1,x,y;i<=k;++i)
			scanf("%d%d",&x,&y),a[x].data=y;
		printf("%d\n",solve());
	}
}

[PKUSC2021] 一棵树

首先如果你不看数据范围,你是会被这个 \(k\) 给吓到的,实际上 \(k\le 1\)

posted @ 2021-11-19 20:36  Point_King  阅读(132)  评论(0编辑  收藏  举报