8.17 模拟赛记录 & 数论知识点补充

8.17 模拟赛记录 & 知识点补充

不得不说,数论重构确实快

复盘

u1s1,考数论多少有一点担心,毕竟数论万一考一些非常阴间的同余方程期望组合数学之类基本连个暴力都写不出来,但是总归问题不大,考他就完了,实在不行可以像昨天一样冲正解233
开局遍历,T1显然有某种规律,经过一番推导,发现每一个数x在下一次计数产生的贡献都是(x+1)+x+…+2,以此类推。经过一系列纠结和优化,最终优化到了O(n2).觉得还可以优化,时间还够,先放一放。T2是一个几何问题,打眼一看没什么思路,20分的暴力非常好写,也先放一放。T3是一个我见过的问题的加强版,有了经验,觉得这题很可能做出正解,T4b=c可以分类讨论,一次系数也可以做。最终决定从T3做起,此时已经过了将近一个半小时。
在有经验的情况下,通过倒推,T3大约有半小时就做出来了,然后回去打T2T4的部分分。T2没啥问题,但是发现T4其实没有想象中那么简单,贪二次项有些困难,最终就写了两个特殊限制。最后是T1,写出一个n<=5以前都对的转移,这时候时间差不多也就到了,交卷。
这场比赛整体上时间比较紧张(摸键盘的时候考试就剩两个小时多一点了),但是节奏并不是很紧张,没出现之前调样例一直到时间截止都没调完的现象,算是有进步。
预计得分:60+20+100+15.

复盘分析

得分:0+20+30+0.
首先T3有一些问题,虽然考虑到的例子已经够多了,但是由于推的有点问题,一个情况错了,所以挂掉70分,有点遗憾。T1我觉得应该是取模的问题,以及看了一下别的人写的O(n2)写法,感觉比我写的简洁非常多了,只能说代码实现还得练练啊(捂脸)。正解是卡特兰数,在很遥远的一次考试的时候看题解见过(那个年代我周日还能去机房写代码),基本没印象了,没啥可说的。T4还是有一些情况没考虑进去,这主要是题意理解问题。总之这次其实没有发生很多暴力莫名其妙爆零的事情,这算是一个很大的进步,但是做数论思路确实得更完善才行。

题解

T1其实是一个卡特兰数问题。
卡特兰数其实就是C(2n,n) / (n+1) = (2n)! / (n!(n+1)!)。
问题来了,怎么判段一个方案数的问题是否符合卡特兰数?

其实最靠谱的方法是打表看是否符合。(学了一年OI,我充分明白了什么叫实践是检验真理的唯一标准)

事实上上面这个式子是最终的结果,真正的卡特兰数的完整表示应该是一个函数C(x),满足C(x)=C(x-1)C(0)+C(x-2)C(1)+…+C(0)C(x-1),
也即C(x)=(sigma)(C(i) + C(n-i-1)) (0<=i 这个式子就可以拿去判断一个问题是否符合卡特兰数了。
看了一大圈讲卡特兰数的博客,我认为,卡特兰数问题的基本应用有两个,一是出入栈方案问题,二是从(1,1)走到(n,n),只经过对角线一侧的方案数。
在研究T1之前,先来研究两个问题,看一下怎么试着往这两个问题上转化:

sub1 有0,1各n个,求长为2n的01序列中,任意前缀当中0都不比1少的方案数。
此题当中,假如我们把01转化为两维,加一个0就向右走一步,加一个1就向上走一步,那么前缀中0不少于1,在这个图像中就表现为整个图象在y=x下方,这就转化为基本问题2.

sub2 有n个拿5元和n个拿10元的人买10元的东西,售货员没有零钱,只能用5元给拿10元的人找零。求有多少种排队方案,使得售货员可以给2n个人全部正确找零。
每个拿5元的都能给出5元,每个拿10元的都要消耗5元,这就转化为基本问题2.从栈的角度考虑,给出5元相当于把5元入栈,消耗5元相当于把5元出栈,这就转化为基本问题1.

现在回头看这个问题,我们递推的思考,n多1的时候,第一个n只能放在第一个(n-1)及以后,第二个n只能放最后。这时我们发现,其实这个规律和这个序列进出栈的规律完全一致(把前面的1~(n-1)弹出一组,压进一组的情况下n最早出栈;把前面的两组全都进出栈n最晚出栈),满足基本问题1.

由此可见,哪一种更好用不一定。其实还是打表容易。

以及此题需要用上逆元,由于n!-1 = n * (n+1)!-1,可以快速幂求(2n)!,然后就可以线性求n!~(2n)!的逆元了。

T2(洛谷 UVA1742)是一个几何问题,其实并不是非常复杂,大概是考试的时候太谨慎了,没有写上去一个什么,其实我想到的复杂度已经基本达到正解水平了,只不过实现上差一些。
这题很显然的一个性质是,这条直线肯定从某端点出发最好。所以对于除与其同一高度的线以外,剩下的都可以用一个斜率区间表示直线可以达到的范围,取一个权值最大的就行了。这个范围可以上线段树维护,复杂度O(n2logn).
然而线段树需要离散化,不好写,这里学到另一个解法:尺取法。

首先不得不说,我虽然学会了尺取法,但是复杂度证明没想好,应该是n->logn,有待研究。

尺取法说白了其实就是贪心,保留这些区间的左右端点,可以通过定向的推进这两维求出正解(分别按左右端点排序),这样定向推进就能去掉很多的无效枚举。然而这也意味着尺取法会丢掉大量的情况,万一不满足性质就会丢解,所以必须合理证明可以用尺取法再用。
那么什么时候可以用尺取法?在看了一大圈讲尺取法的博客之后(似曾相识),基本可以概括为:只有在得到了当前区间的答案,就知道下一步区间怎样移动时,才能用尺取法,例如合法最短区间、合法区间个数等。一个常见的用法是反复移动右端点直至出现合法解,然后移动左端点,缩小这个范围,过程中更新答案;如果再次非法,继续移动右端点,以此类推。
还是先看一道例题:

sub1 已知一个由正数组成的序列,求其和不小于给定的数S的最小连续子序列长度。
此题记子序列的左右端点,如果这段的和小于S,要右移端点;如果已经大于S,可以试着从左边去掉元素。这就可以利用尺取法求解了。

来看一下这题,我们想找一个点使得覆盖的线段权值尽可能大。初始取当前点所在位置为0(虽然端点实际在某线段上,但不要忘了此时已经离开原二维图象了),把所有线段按左端点排序,每次我们取一个区间,当我们取的区间变多,权值就会变大,点就不断左移,如果点离开了一个区间,一定是比右端点大。所以把原来的复制一遍再按右端点排序,每次发现点的位置(当前加入的左端点)比最小的区间右端点还小,就扫按右端点排序的下一个区间,如此循环。很明显我们的区间移动是在得到答案的时候就知道怎么处理的(事实上其实移动根本就和权值没关系),所以符合尺取法。这样写复杂程度比线段树就低多了。
此题还需要注意,不存在平行于x轴的取法而存在平行于y轴的取法,为了避免斜率不存在的情况,应该取斜率的倒数存储。
完整代码:

#include
#include
#include
using namespace std;
const double eps = 1e-8;
const int N = 2e3 + 1;
struct yjx{
	double L,R;
	int id;
}a[N],b[N],emp;
bool cmpL(yjx x,yjx y){
	return x.L < y.L;
}
bool cmpR(yjx x,yjx y){
	return x.R < y.R; 
}
double num[N][3];
int main(){
	int i,j,n,cnt,temp,x,y,z;
	long long res,sum;
	//while(scanf("%d",&n) != EOF){
		scanf("%d",&n);
		emp.L = emp.R = emp.id = 0;
		for(i = 1;i <= n;i++){
			scanf("%d %d %d",&x,&y,&z);
			num[i][0] = x,num[i][1] = y,num[i][2] = z;
			if(num[i][0] > num[i][1]) swap(num[i][0],num[i][1]);
		}
		res = 0;
		for(i = 1;i <= n;i++){
			cnt = 0; 
			for(j = 1;j <= n;j++){
				if(num[i][2] == num[j][2]) continue;
				a[++cnt].L = (double)(num[i][0] - num[j][0]) / (num[i][2] - num[j][2]);
				a[cnt].R = (double)(num[i][0] - num[j][1]) / (num[i][2] - num[j][2]);
				//得到斜率区间
				a[cnt].id = j;
				if(a[cnt].L > a[cnt].R) swap(a[cnt].L,a[cnt].R);
				a[cnt].L -= eps,a[cnt].R += eps;//这个操作和扫描线防止重合有异曲同工之妙
				b[cnt] = a[cnt];
			}
			sort(a + 1,a + cnt + 1,cmpL);
			sort(b + 1,b + cnt + 1,cmpR);//尺取法预处理
			sum = num[i][1] - num[i][0];
			res = max(res,sum);
			temp = 1;
			for(j = 1;j <= cnt;j++){
				sum += num[a[j].id][1] - num[a[j].id][0];//移动到下个区间左端点
				while(b[temp].R < a[j].L){
					sum -= num[b[temp].id][1] - num[b[temp].id][0];
					//把能覆盖的右端点右移
					++temp;
				}
				res = max(res,sum);
			}
			for(j = 1;j <= cnt;j++) a[j] = b[j] = emp;//清空
		}
		printf("%lld\n",res);
	//}
	return 0;
}

T3是一个博弈论问题,此题头儿必须得一个钻石的情况下,经过打表可知,每隔一个人给一个钻石就可以了,所以结果就是n/2上取整。如果头儿没有钻石,会复杂一些。首先奇数个比较容易解,一定是n/2下取整。对于偶数,假设n=4,一个钻石都没有,如果头儿被踢掉,n=3的时候第二个人一定会被踢掉(后两个人没有钻石一定始终投反对票),由于优先保命,n=4没钻石的情况下前两个人会同意,合法。往下继续推导,只要是n=2k,没有一个钻石的情况下一定有一半的人同意(不然都会被踢掉)。对于其他偶数的情况,关键在于能否最终归到n=2k的情况,经过一番打表分析(u1s1这表不好打),这时候需要的钻石数应该为去掉一个2k除以2的情况。这样一来分类讨论就行了。

T4(洛谷CF493E)是一个数论问题,我不太能说的很清楚,引用一段题解。

设我们所求的多项式为 ,如果存在,考虑其常数项v ,我们有v ≡ c (mod b),而由于非负整数系数的限制,我们有v ≤ f(a) ≤ b,因此我们可以得到常数项v 的可能值,之后b − v,c − v 分别应是初始a,b (下记为a0,b0)的整数倍。
如果b0 > 1,则令b − v,c − v 分别约去a0,b0 后,这个问题的规模就更小了,转化为求多项式g(x) = (f(x)-v) / x,其中b = g(a0) ,c = g(b0).
而当a = b = c = 1 的时候,此时任意一个幂函数均符合要求,答案为-1,其它情况下都可以得到答案为0。
上述约去的过程可以证明如果答案存在且有限,则最多存在两个解(当a =
b = c > 1 的时候,一个为常数函数,一个为恒等函数),在每一步求最低阶系
数的时候,只有两种情况,系数为常数的情况还要求当前的b,c 值相等,因此递归需要进行的次数最多也是log 级别的。

以我的理解,就是每次去掉合法的常数,这时候可以约掉一维,问题仍然不变,如此反复。特殊情况本质都是一样的,可以从初始的特殊情况推导得出过程中特殊情况应该怎么处理。
代码如下:

#include
#include
#include
using namespace std;
const int mod = 1e9 + 7;
long long A,B,C,cnt,res,d[100001];
bool solve(long long b,long long c){
	long long v;
	int temp;
	bool go;
	if(!b || !c){
		if(!b && !c){
			res = (res + 1) % mod;
			return 1;
		}
		return 0;
	}
	v = c % B;
	if(v){
		if((b - v) % A == 0){
			d[++cnt] = v;
			return solve((b - v) / A,(c - v) / B);
		}
		else return 0;
	}
	else{
		go = 0;
		temp = ++cnt;
		if(b % A == 0){
			d[temp] = 0;
			go = solve(b / A,c / B);
		}
		if(b == B && c == B){
			res = (res + 1) % mod;
			if(!go){
				d[temp] = B;
				cnt = temp;
			}
			return 1;
		}
		return go;
	}
}
int main(){
	int i,t;
	//scanf("%d",&t);
	//while(t--){
		res = 0,cnt = -1;
		scanf("%lld %lld %lld",&A,&B,&C);
		if(B == 1){
			if(C == 1){
				if(A == 1) printf("inf\n");
				else printf("1\n");
			}
			else printf("0\n");
			return 0;
		}
		solve(B,C);
		res = (res - 1) % mod + 1;
		printf("%lld\n",res);
		if(!res) return 0;
		if(cnt == -1){
			d[++cnt] = B;
		}
		//printf("%lld ",cnt);
		//for(i = cnt;i >= 0;i--) printf("%lld ",d[i]);
		// printf("\n");
	//}
	return 0;
}

Thank you for reading!

你可能感兴趣的:(记录)