首页 > 技术文章 > qbxt Day 3

57xmz 2020-10-03 23:49 原文

Day 3

考试题解

T1 欢乐(D2T1)

签到成功+3。考试的时候想了一个非常阴间的做法:\(N!/k!\)\(k!\)其实刚好能够拼凑成长度为\(N\)的一段序列,那么我将这段序列划分成两部分,从1开始的那一部分就是\(k!\),另一部分则是\(N!/k!\)由于我太怂了,害怕正解打错,所以测试点分治了,前10个点我跑暴力。于是我从11开始找。10的时候\(k=7\)\(n=11\),我用一个\(double\)变量\(sum\)来存\(N!/k!^2\),那么很显然,如果\(sum\)大于1,则说明\(k\)取小了,那么我就让\(sum\)除以\(k^2\)\(k++\),然后再判定,如果还小就再加,再除以。直到符合条件的时候,就继续找下一个\(n\)\(sum\)乘上\(n\),然后再进行判定。直到\(n==N\),结束循环。此时可以发现,\(n\)\(N\)的复杂度是\(O(N)\),同时发现\(k\)是单调递增的,不可能往回减。所以最多也只能增加到\(N\)。这样总时间复杂度就是\(O(n+n)\)。写完之后拍上了,而且过了老师的大样例,非常开心,觉得想出了正解。没想到还有对数如此神奇的比较大小的操作。不管怎么样,还是签到成功了。

80分做法:

高精度

100分做法:

比较\(a\)\(b\)的大小不一定需要直接比较,可以比较\(lga\)\(lgb\)的大小。\(lgN!=lg1+lg2+...+logN=log(N-1)!+lgN\)

那么\(N!/k!\)\(k!\)比较大小直接取\(lg\)即可,再维护前缀和即可\(O(1)\)查询

拓展:洛谷 \(P4370\) 组合数问题2

画出杨辉三角,\(C_n^{n/2}\)是最大的,然后找它左上,左边,右上,右边的组合数,分别比较大小,然后扔到大根堆里。用\(log\)比较大小。

T2 水题(T3难度,D1T3)

如果除去输入的阴间操作,那这题就是道模拟水题。拓扑序模拟

考试题目不一定按照难度升序排列,不要一直死磕一道题。

读入:每有一个“;”隔开两个点,所以可以先数一遍有几个“;”,就可以得到有几个点。同时做的任务倒着读入进来即可。首先处理任务名字,然后处理依赖关系。

可以练习代码能力。

(应该要用\(Hash\)来建立字符串和整型变量的一一对应,但是我不会。。。)

T3 模拟(T4难度,D2T3)

20分做法:

算出长度,有多少个0和多少个1,然后就一共有50!/长度*每一段的0的个数或1的个数

40分做法:

长度小于等于10,直接求出最小公倍数(在复杂度允许的范围内),然后长度都搞成一样的,直接暴力枚举即可。

60分做法:

互质的时候根本不用关系整个字符串长什么样,会有一个性质,每一位数都会和第二个字符串的每一位相加一次,那么只需要数出来每一个数有多少个0或1然后乘法原理即可

100分做法:

如果不互质的话发现之前结论不成立了,那么将两段除以最大公约数,然后转化成互质的两段,分成不同的块。统一公因数。

T4 赛(T2难度,D1T2)

30分做法:

五层循环暴力

60分优化:

将左右区间去掉,换一种思想,算一个“V”字形被几个区间包含。考试时做法,没想到卡到了80分。

80分做法:

j左边的数取哪个跟右边取哪个没关系,相当于左右两半分别循环。暴力一遍找出大于\(a_j\)的数,开结构体记录,然后用两个求和搞出来。复杂度为\(O(n^2)\)

100分做法:

用权值线段树来记录大于当前值的下标之和,正着for一遍,反着再for一遍,然后乘法原理乘起来就是最后的答案。设中心点下标为\(j\)。那么设左边比\(a_j\)大的序列是\({x_1,x_2,...,x_k}\),设右边比\(a_j\)大的序列是\({y_1,y_2,...,y_l}\)。它们的下标分别是\(X_1\)\(Y_1\)。假设我们取出\(x_1\)\(y_1\),那么需要看看它们被几个区间所包含,其实就是看它们的下标。\(x_1\)的左边包括自己有\(X_1\)个数,而\(y_1\)右边有\(n-Y_1+1\)个数。那么根据乘法原理,可以得出这两个分别在左右两边的数总共贡献了\(X_1*(n-Y_1+1)\)个合法的三元对。就这样推导下去,化简就会发现,实际上就是左边序列的下标和乘上右边序列下标和,然后正反分别求两遍,再将左右两边乘起来取模就是最终答案。

然后考虑如何实现:因为左边序列如果向右一直扩展,之前的数其实还是在的,没有改变,相当于往区间里加数。而且还要支持区间求和,不禁让人想到支持区间修改和区间求和的线段树和树状数组。这里有一个比较阴间的东西(可能是我太菜了):权值线段树。这个东西在本题中怎么使用呢?注意到每个元素的大小都是不超过\(10^5\)的,那么我们就以每个元素的大小作为线段树元素下标,然后将这些元素的下标作为线段树元素的权值。这样的话就会发现神奇的一点:每次往区间里扔一个数,实际上就是将线段树中下标为这个元素大小的那个数的权值加上这个数的下标(有点绕)。这样就相当于是单点修改。然后区间求和就更简单了,因为是求大于这个数的下标之和,直接将线段树内下标大于这个数的大小的下标和输出即可。右序列倒着枚举就行。每次单点修改和区间查询都是\(O(logn)\)。再加上枚举中间的点,总时间复杂度是\(O(nlogn)\)

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
typedef long long ll;
inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
int n,sum,a[1000005],f1[1000005],f2[1000005],maxx;
const int mod=1e9+7;
struct Node{
	ll v,tag;
	int l,r;
	Node *ls,*rs;
	Node(const int L,const int R){
		l=L,r=R;
		if(L==R){
			tag=0;
			v=0;
			ls=NULL;
			rs=NULL;
		}
		else{
			tag=0;
			int mid=(L+R)/2;
			ls=new Node(L,mid);
			rs=new Node(mid+1,R);
			pushup();
		}
	}
	inline void pushup(){
		v=ls->v+rs->v;
	}
	inline void pushdown(){
		if(tag==0) return;
		else{
			ls->maketag(tag);
			rs->maketag(tag);
			tag=0;
		}
	}
	inline void maketag(ll w){
		v+=(r-l+1)*w;
		tag+=w;
	}
	inline bool InRange(const int L,const int R){
		return (l>=L)&&(r<=R);
	}
	inline bool OutofRange(const int L,const int R){
		return (l>R)||(r<L);
	}
	inline void upd(const int L,const int R,ll w){
		if(InRange(L,R)){
			maketag(w);
		}
		else if(!OutofRange(L,R)){
			pushdown();
			ls->upd(L,R,w);
			rs->upd(L,R,w);
			pushup();
		}
	}
	inline ll qry(const int L,const int R){
		if(InRange(L,R)){
			return v;
		}
		if(OutofRange(L,R)){
			return 0;
		}
		else{
			pushdown();
			return ls->qry(L,R)+rs->qry(L,R);
		}
	}
};
int main()
{
	n=read();
	for(register int i=1;i<=n;i++){
		a[i]=read();
		maxx=max(maxx,a[i]);
	}
//	printf("%d\n",maxx); 
	Node *rot1=new Node(1,maxx);
	Node *rot2=new Node(1,maxx);
	for(int i=2;i<=n;i++){
		rot1->upd(a[i-1],a[i-1],i-1);//存入下标
		f1[i]=(rot1->qry(a[i]+1,maxx))%mod;
	}
	for(int i=n-1;i>=1;i--){
		rot2->upd(a[i+1],a[i+1],n-i);
		f2[i]=(rot2->qry(a[i]+1,maxx))%mod;
	}
//	for(int i=1;i<=n;i++){
//		printf("%d %d\n",f1[i],f2[i]);
//	}
	for(int i=1;i<=n;i++){
		sum=(sum+f1[i]*f2[i])%mod;
	}
	printf("%d\n",sum);
	return 0;
}

动态规划

Problem 22

种一次

首先,可以发现跟\(m\)年的种地顺序没有关系,所以可以改变种地顺序。按照左端点从小到大排序。

每种一段地的真实收益\(w_1=p_1-a_l-a_{l+1}-...-a_r\)。用\(f_i\)来表示种了的地最靠右为\(i\)。状态转移方程:\(f_{r_i}\)=\(max\)(\(f_1\),\(f_2\),...,\(f_{l_{i-1}}\))+\(w_i\)。通过维护前缀最大值实现\(O(1)\)转移。总时间复杂度为\(O(n)\)

种多次

分三种情况讨论:(设\(r\)是已经种过的地最靠右的位置)

1.\(r<l_i\) \(f_ri=max(f_1,...f_r)+w_i\)

2.\(l_i<=r<=r_i\) \(f_{r_i}\)=\(max(f_{l_i},...f_{r_i})+p_i-(a_r+1+...+a_{r_i})\)

3.\(r>r_i\) \(f_r=f_r+p_i\)

1.2 需要求出区间最大值;3. 需要对区间进行修改。所以可以用线段树这个数据结构进行优化,总时间复杂度\(O(nlogn)\)

DP思路:先想出暴力做法(一般是n^2),然后再考虑数据结构优化。

Problem 21

九维dp我直呼内行。不过没听懂。fuck

Problem 20

类似于背包

\(f_{a,i,j,k,c,0/1}\)表示已经过去了\(a\)个人,选了\(i\)个守门员,\(j\)个前锋,\(k\)个后卫,已经花费了\(c\)元,0/1表示队长选了还是没选。但是这样做不好输出方案数。由于队长一定是价值最大的,所以可以在转移之前先排序,从小到大排序之后再\(dp\),最后那个人一定是队长。

Problem

中间的连线要被走,\(n_i\)表示第\(i\)棵树大小,\(f_i\)表示第\(i\)棵树的答案。\(g_{i,j}\)表示在第\(i\)棵树所有点走到\(j\)的距离之和。

Problem 10

\(f_{i,j}\)代表选了\(i\)个数,\(gcd=j\)。分成两种情况讨论,如果\(gcd\)改变,那么肯定是\(f_{i+1,gcd_{j,a_k}}\),如果\(gcd\)不改变,用\(O(n)\)暴力枚举出j的倍数一共有多少个,如果当前选的数的个数比它小,那么还有的选,就可以转移。

博弈论\(dp\),每一个值都是\(true\)\(false\)

。。。

\(n\)个点,深度为\(k\)的二叉树有多少种?

\(f_{i,j}\)表示有\(i\)个点深度为\(j\)。枚举左右子树,左子树有\(k\)个点,右子树就有\(i-1-k\)个点。然后分左子树深度为\(j-1\)时和右子树深度为\(j-1\)时,时间复杂度是\(O(n^4)\)

换一种状态:\(f_{i,j}\)表示有\(i\)个点深度小于等于\(j\)\(f_{i,j}\)=\(sigma\)(\(k=0\) \(to\) \(i-1\)) \(f_{k,j-1}*f_{i-1-k,j-1}\)。最后答案就是\(f_{n,k}-f_{n,k-1}\)。时间复杂度是\(O(n^3)\)

推荐阅读