牛客day1的补题和总结

C和I是签到题,没什么好说的。

从A开始。
A读题读了我20分钟,我才把样例弄懂。。
这题目比cf阴间一佰倍,主要也是这类题的题面就是麻烦,有时候中文的题面的也能让我写一半回去再读几遍。

这个主要就是写太慢了。完全可以更快的,而且这个思路我觉得大部分其实是lhgg帮我出的,我自己的思路挺少 。
开始的时候的思路是啥来着。。太久了忘记了。也是当时想了太多了,记不下来。不过第一步出的思路是枚举从\(1\)\(n\)
我们用了几个奇数来满足要求,也就是最后一位为0的数字,然后把这些数字二进制拆开,竖着看,大概是这样

... ... ... ... ...

... ... ... ... ... 这个是用了3个数字满足and起来是1的条件的情况,也就是\(i=3\)的情况,这个目前只满足了前三个数字and起来第一位是0

1 1 1 0 0 0 还有的要求是上面and起来必须等于0,而这个只需要一整行没有全部是1即可。这个的方案数量就很明显了,是

\((2^{i}-1)^ {m-1}\),就是底下是1的所有空位随便填0或者是1的方案数,其中括号内的\(-1\)的含义是因为我们需要排除整行都是1的情况,\(m-1\)
\(-1\)的含义是我们只需要填\(m-1\)行,最下面的一行已经确定了。
然后就是后面的底下是\(0\)的位置要如何填,这个其实更简单,因为我们只需要选择子数组,所有是真正的任意填写。\(2^{(n-i)\times m}\)这样前面和后面的所有的位置都已经填写完毕了,如果只是分别独立的看他们两个,在他们的内部是没有顺序的区别的。因为我们是直接枚举了所有的数字,他们即使重复也无所谓,因为顺序也已经被枚举完毕了。接下来就是要安排他们混在一起之后的方案数。这里是插板法,但是因为本身已经携带了顺序所产生的方案数,所以我们假如是把底下为1的插入底下为0的,那么我们只能把下一个插入的插在上一个被插入的后面的位置,而不能是前面,否则会导致顺序的混乱,使得我们计算的方案数有重复。这个其实不难,就是\(C_n^i\),很清晰的式子。原因的话,是板子+球的数量刚刚好是n个,我们原本的板子自带顺序,所以只需要找到\(i\)个板子的位置即可。所以就是\(C_n^i\),就是n个位置里面选出\(i\)个位置,他们是板,而剩下的是球。然后上面的三部分乘起来,就是答案。

\(\displaystyle\sum_{i=1}^{i\leq n}((2^{^i}-1)^{m-1}\times 2^{(n-i)\times m} \times C_n^i)\)

然后模数很阴间,导致我们wa了3发,最后是直接暴力递推过的,都用费马反向递推算逆元。。。费马也用不了。

这题我其实不能自己推导出来挺不应该的。难吗?真不难,我开始的时候出的思路就挺对的,但是后面一直在纠结重复的问题,思考也变得不顺畅了。其实就是做这类题目做少了,要是有一定的刷题量保证,做的时候就不会这么艰难,很多时候也有脑子去继续往下想了。也许这就是三人配合的意义?我可以不用把前面的过程装在脑子里继续往下想,确实给了我挺多的空间的。

这次能学到的其实是顺序吧,对于这个的理解需要更深一步了,然后就是那个\(C_n^i\),当时我就是卡在这里了,我手上和脑子里面用于这个东西太少了,完全达不到我其他板块的能力,做起题来就有很深刻的感受。距离融会贯通不知道有多远。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,q;
inline int exgcd(int a,int b,int& x,int& y) 
{
    if(!b) { x=1,y=0; return a; }
    int d=exgcd(b,a%b,x,y);
    int z=x; x=y; y=z-a/b*y;
    return d;
}

inline int inv(int a,int m)//a在mod m意义下的逆元
{ 
    int x,y,d=exgcd(a,m,x,y);
    return d==1?(x%m+m)%m:-1;
}
int q_pow(int x,int y)
{
	int su=1;
	while(y)
	{
		if(y&1)
		{
			su=su*x%q;
		}
		x=x*x%q;
		y>>=1;
	}
	return su;
}
int C[5010][5010];
void init()
{
	C[1][1]=1;
	for(int i=2;i<=5001;i++)
	{
		for(int j=1;j<=i;j++)
		{
			C[i][j]=(C[i-1][j]+C[i-1][j-1])%q;
		}
	}
}
signed main()
{
//	cout<<inv(2,4)<<endl;
	scanf("%lld%lld%lld",&n,&m,&q);
	init();
	int su=0;
	for(int i=1;i<=n;i++)
	{
		int g1=(q_pow(2,i)-1+q)%q,g2=q_pow(2,n-i);
//		cout<<g1<<" "<<g2<<endl;
		su+=q_pow(g1*g2%q,m-1)*C[n+1][i+1]%q;
		su%=q;
	}
	cout<<su<<endl;
}

D 题

原本赛时的想法是探究下加法和异或的分配律,但是经过手玩之后发现时间复杂度依旧不正确并且难写。
题解一直看不懂,因为其实真正要解决的问题是,维护时,由于我们异或起来的所有数字都会因为最后一个数字的修改而变动,这使得每次询问时,维护所需的时间复杂度很爆炸。
在你没有注意到前缀和的时候也没法解决。而前缀和其实也具有一定的隐蔽性,因为即使注意到了使用前缀和,那也只是使得描述变得简单而已。并没有直接解决上面所说的,维护的时候需要修改全部的异或部分的问题。
而事实上,这个很明显是一个区间查询和区间修改的问题,线段树能过做到区间查询和区间修改,但是它在区间修改的基础上快速维护区间查询则是基于你修改操作和查询操作之间简单的分配律和结合律。这也是异或操作无法直接和加法修改进行维护的根本原因。他们之间不存在合法的十进制运算的结合律和分配律。而最好想到的思路是拆位维护,而你在二进制下的异或和加法的结合律会涉及进位操作,直接让时间复杂度爆炸,而且难写难想。这个就是我死掉的赛时的思路。

而正解的思路可以说是和我的完全不一样。首先就是先使用前缀和来表示我们所维护的东西。这里用\(\oplus\sum\)表示异或和。
我们所求的就是\(\oplus\sum(S_n-S_i)\),其中\(S_i\)表示前缀和。这样做,我们可以发现,虽然每次修改导致所有的异或参与部分发生改变这个事情没有变化,但是改变的部分变成了括号内的一个小部分。但是括号依旧无法拆开,这里需要连续两次的操作。
我们考虑将异或操作进行转化。拆位考虑,对于第\(d\)位,我们只需要找到对于所有的\(S_n-S_i\),它的第\(d\)位有几个1即可。而这个时候,最重要的来了,也是如何拆开括号的部分。其实对于\(S_n-S_i\)的第\(d\)位是否有1,也就是在询问\((S_n-S_i)\ mod\ (2^{d+1})\)是否\(\geq\)\(2^d\),如果这个式子成立,那么,\(S_n-S_i\)的第\(i\)位也就是\(1\),否则就是\(0\)。而我们高兴的发现,加法和减法,对于\(mod\)运算,在任何进制下都是有合法的分配律的,也就意味着,这个括号被拆开了。

我们原本需要维护的是\(\oplus\sum(S_n-S_i)\),根据上面的,写出来的就是,对于第\(d\)位,询问\(count((S_n-S_i)\ mod\ (2^{d+1})\geq2^d)\)的数量。
其中的\(S_n\)在变化,我们将式子再次操作\(count((S_n\mod 2^{d+1}-S_i\ mod\ 2^{d+1})\geq 2^d)\)我们可以发现,原本的一个区间修改变成查询的时候的一个限制了。而我们的添加操作就是一个单点修改,查询则是在查询值域,这个是树状数组的活了。
让我们再次移动项,因为mod的原因,我们无法在树状数组里面进行加减操作,我们把\(S_n\mod 2^{d+1}\)移动至右边,因为在单次查询中,这一项没有变化。\(count((-S_i\ mod\ 2^{d+1})\geq 2^d-S_n\mod 2^{d+1})\),至此,就结束了。

对于单次修改,我们就只需要维护当前的所有数字的和,以及树状数组里面的第\(n\)个数字,\(- S_n\mod 2^{d+1}\),查询时,则是查询有多少个数字大于\(2^d-S_n\mod 2^{d+1}\) 。负数不好操作,具体操作需要给这个不等号两边同时乘以\(-1\)

这题最难的,就在于这个异或转化\(mod\)
其实首先是拆位,异或其实是在问第\(d\)位有几个\(1\),而后是,第\(d\)位有几个\(1\),又可以转化为问有几个数字满足$ a\mod 2^{d+1}\geq 2^d$,而这个东西,是可以用值域上的树状数组维护的,最好的在于,mod是有合法分配律的,它和普通的四则运算有联系。上面的东西也就可以拆开维护了。

这题,最能学到的还是这个异或转化。这个转化和树状数组的联系很大啊,要是没写过类似的题我是完全是想不到的,算是树状数组的一种运用了。
这个前缀和的方法也是非常重要的,其实在可持久化trie树的时候我做过挺多的,这种区间查询转化为前缀和的操作。这次没想到反正有点不太应该了。

哦,题面还有一个提醒,是模数,这个数字是\(2^{20}\),非常的真好,如果没有这个数字,可能会复杂度爆炸,但是其实限制一下值域也可以出题,可能启发性比这个强一些吧。

额,除了这些,其实取模情况下的查询也是需要特殊处理的。我咋之前一点没学过啊?难蚌
上面的式子无法直接写入代码,其实仔细思考一下,就会发现,左边的加法存在负数的情况,我们可以拆开,但是最好不要随便的变号和移项。其实思路就是,什么样的数字在\(mod\ 2^{d+1}\)意义下被前面的部分减去是\(\geq 2^d\)的,手玩一下,就可以发现,是一个连续的区间范围,我们只需要查询这个区间范围有多少数字即可。具体的范围公式见代码。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read(){
	int a=0,b=1;char c=getchar();
	for(;c<'0'||c>'9';c=getchar())if(c=='-')b=-1;
	for(;c>='0'&&c<='9';c=getchar())a=a*10+c-'0';
	return a*b;
}
int n,a[500001],b[500001],s[500002],q,t,v;
int tr[3000001],An[500001];
inline int lowbit(int x)
{
	return x&(-x);
}
inline void add(int i,int x)
{
//	if(i==0)cout<<"?"<<endl;
	i++;
	while(i<=(1<<21))
	{
//		cout<<i<<endl;
		tr[i]+=x;
		i+=lowbit(i);
	}
}
inline int ask(int i)
{
	i++;
	int ans=0;
	while(i>0)
	{
//		cout<<i<<endl;
		ans+=tr[i];
		i-=lowbit(i);
	}
	return ans;
}
int main()
{
	q=read();
	for(int i=1;i<=q;i++)
	{
		a[i]=read(),b[i]=read();
	}
	for(int d=0;d<=20;d++)
	{
		a[++n]=0;add(0,1);
		for(int Q=1;Q<=q;Q++)
		{
//			cout<<a[Q]<<endl;
			for(int j=1;j<=a[Q];j++)add(s[n],-1),n--;
			ll now=s[n]+b[Q];
			now%=(1<<(d+1));
			int cnt=0;
			ll l=(now+1)%(1<<(d+1)),r=(now+(1<<d))%(1<<(d+1));
			if(l<=r)
				cnt+=ask(r)-ask(l-1);
			else 
				cnt+=((ask(r))+(( ask((1<<(d+1)))-ask(l-1))));
			if(cnt%2==1)An[Q]+=(1<<d);
			n++,s[n]=now;
			add(s[n],1);
		}
		while(n)add(s[n],-1),n--;
	}
	for(int i=1;i<=q;i++)
	{
		cout<<An[i]<<endl;
	}
	return 0;
}
posted @ 2024-07-18 10:44  HL_ZZP  阅读(5)  评论(0编辑  收藏  举报