NOI Online #3 提高组 小记

前言(成绩尚未出来时的开坑语)

绝不放过任何一个可以水的机会。

前两次比赛感觉都在瞎打(主要是上了太久网课都没刷过题,脑子锈了?),最后基本上都是压线,还被叶老师教育了一番。

这次也停了半个多月的课了,想来还是有些进步的吧?

实际上感觉手开(O2)之后最后两题还是有些希望的。。。

说到希望,在此贴上那段名句吧:

那就是希望

即便要取模,也是光明

(T1):水壶(kettle)

这应该是一道大水题吧。。。

显然由于最后只能选一个水壶,考虑枚举选哪一个水壶,就会发现肯定是依次对该水壶之前(k)个水壶执行操作最优。

也就是选择连续的(k+1)个水壶。

那么直接前缀和+暴力枚举就可以了。

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 1000000
using namespace std;
int n,k,a[N+5];
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define D isdigit(c=tc())
		char c,*A,*B,FI[FS];
	public:
		I FastIO() {A=B=FI;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=(x<<3)+(x<<1)+(c&15),D);}
}F;
int main()
{
	freopen("kettle.in","r",stdin); freopen("kettle.out","w",stdout);
	RI i;for(F.read(n),F.read(k),i=1;i<=n;++i) F.read(a[i]),a[i]+=a[i-1];//前缀和
	RI ans=0;for(i=k+1;i<=n;++i) a[i]-a[i-k-1]>ans&&(ans=a[i]-a[i-k-1]);//暴力枚举
	return printf("%d",ans),0;
}

(T2):魔法值(magic)

非正解开O2水过去的一道题。

看到这种东西一眼矩阵乘法吧。。。

我们设置一个转移矩阵,其中第(x)行第(y)列表示点(x)对点(y)的贡献。显然若存在边((x,y)),则矩阵第(x)行第(y)列为(1),否则为(0)

在普通的矩乘中,我们的转移式为:

[f_{i,j}=sum_{k=1}^nf_{i,k} imes f_{k,j} ]

而在这题中,由于我们考虑的是异或值,也就是说只用知道最终贡献的奇偶性即可。

于是可以把加法和乘法分别表示为二进制下的位运算,即:

[f_{i,j}=xor_{k=1}^n(f_{i,k} and f_{k,j}) ]

然后我们来看看数据,发现由于多组询问,这种做法是(O(n^3cdot qcdot32))的,会被卡掉。

所以我们就不得不考虑如何优化。

于是就发现,既然这是一个(01)矩阵,而且转移是借助位运算进行的,因此可以使用(bitset)

也就是我们令(p1_i)表示第(i)行的(bitset)(p2_j)表示第(j)列的(bitset),转移得到的第(i)行第(j)列的值变成了:

[Count(p1_i and p2_j) ]

其中(Count(x))表示(x)二进制下(1)的个数的奇偶性,其实就是上式中每一位异或起来的值。

码完调过样例之后测了测极限数据,发现依然(T)了,跑了(1.4s)

细细一想,调用系统自带的(bitset)常数很大(大概?我猜的),反正这题(nle100),直接用两个(unsigned long long)似乎就能起到(bitset)的作用了。

这样一来复杂度大概是(O(n^2cdot qcdot 32))忽略掉常数,尽管并不能忽略。。。),好像看起来还不错?

于是写了一发,果然快了一些,变成了(1.2s),却依然在绝望的边界上徘徊。

怎么办?上(O2)

于是一下就快了许多,成功跑进(1s),成功水过。。。

#pragma GCC optimize(3)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100
#define UI unsigned
#define ull unsigned long long
using namespace std;
int n,Qt,cnt[65536];UI a[N+5];
#define Set(A,x) ((x)<=64?(A[0]|=1ull<<(x)-1):(A[1]|=1ull<<(x)-65))//修改bitset A第x位的值为1
#define Count(x) (cnt[(x)&65535]^cnt[((x)>>16)&65535]^cnt[((x)>>32)&65535]^cnt[(x)>>48])//拆四段分别判奇偶性
struct M
{
	ull p1[N+5][2],p2[N+5][2];
	I M() {int i;for(i=1;i<=n;++i) p1[i][0]=p1[i][1]=p2[i][0]=p2[i][1]=0;}
	I friend M operator * (Con M& x,Con M& y)//矩阵乘法
	{
		M t;RI i,j,k;for(i=1;i<=n;++i) for(j=1;j<=n;++j)
			Count(x.p1[i][0]&y.p2[j][0])^Count(x.p1[i][1]&y.p2[j][1])&&(Set(t.p1[i],j),Set(t.p2[j],i));//枚举每一位求值
		return t;
	}
	I friend M operator ^ (M x,UI y)//矩阵快速幂
	{
		M t;for(RI i=1;i<=n;++i) Set(t.p1[i],i),Set(t.p2[i],i);
		W(y) y&1&&(t=t*x,0),x=x*x,y>>=1;return t; 
	}
}U,S;
int main()
{
	freopen("magic.in","r",stdin); freopen("magic.out","w",stdout);
	RI m,Qt,i,x,y;for(i=0;i^65536;++i) cnt[i]=cnt[i>>1]^(i&1);//预处理判奇偶性的数组
	for(scanf("%d%d%d",&n,&m,&Qt),i=1;i<=n;++i) scanf("%u",a+i);//读入,注意开unsigned
	for(U=M(),i=1;i<=m;++i) scanf("%d%d",&x,&y),//读入一条边
		Set(U.p1[x],y),Set(U.p2[y],x),Set(U.p1[y],x),Set(U.p2[x],y);//初始化转移矩阵
	UI t,ans;W(Qt--)//处理询问
	{
		for(scanf("%u",&t),S=U^t,ans=0,i=1;i<=n;++i) S.p1[i][0]&1&&(ans^=a[i]);//矩乘后枚举每一个点的贡献
		printf("%u
",ans);
	}return 0;
}

(T3):优秀子序列(sequence)

非正解开O2水过去的又一道题。

显然优秀子序列就是要满足二进制下每一位最多只有一个(1)的子序列,且此处的(sum)完全就等同于按位或的和。

再看看一个子序列的价值要乘上一个恶心的(phi),一看就是在难为我们。

于是,在它的逼(提)迫(示)下,我就自然而然想到了枚举每一种价值计算这种价值的子序列个数。

考虑设(f_i)表示按位或的和为(i)的优秀子序列个数,然后就是如何设计转移的问题了。

一开始首先想到了刷表法(我也不知道为什么,明明我(DP)一般都是用递推法的啊),结果立刻想到了(FWT)

然而一来我已经忘记(FWT)怎么写了,一来我又想不出具体该如何(FWT),最终推了一下就放弃了。

当时比赛的时候这题没有进展就先去优化(T2)了,等到再回来看这题的时候,突然灵光乍现,发现既然刷表不好弄,为什么不递推呢?

即,我们每次枚举(i)的子集(j),就有转移方程:(其中(p_j)表示原序列中(j)的个数)

[f_i=sum_{jsube i}p_j imes f_{i xor j} ]

即每次枚举一个新的数加入,其中枚举子集的复杂度应该是很优秀的。(我比赛时以为是(O(nlogn))的,后来才知道是(O(3^n)),难怪跑这么慢,我又(naive)了)

然后就顺利地过了样例,结果写了个暴力一对拍就挂掉了。。。

为什么呢?找出一组把我(Hack)掉的数据,经过辛苦的调试,才发现这样转移一个序列会因为其中元素加入的顺序不同而被计算多次。

怎么办呢?由于这个序列的特殊性,其中所有非最大值的按位或的和一定小于最大值(因为最大值一定在某个其他数都没有的位上有一个最高的(1))。

所以只要在转移前加一个条件(j>(i xor j))就可以了。

于是这道题就做完了?

实际上我们发现并没有,在极限数据面前,它只能跑(2.2s)

如此眼熟的场面,又一次在绝望的边界上徘徊。。。

熟悉的场面,就用熟悉的解决办法。

怎么办?上(O2)

于是一下就快了许多,成功跑进(2s),成功水过。。。

#pragma GCC optimize(3)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define V 200000
#define X 1000000007
using namespace std;
int n,p[262144],f[262144];
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define D isdigit(c=tc())
		char c,*A,*B,FI[FS];
	public:
		I FastIO() {A=B=FI;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=(x<<3)+(x<<1)+(c&15),D);}
}F;
class LinearSieve
{
	private:
		#define SZ 300000
		int Pt,P[SZ+5],phi[SZ+5];
	public:
		I int operator [] (CI x) Con {return phi[x];}
		I LinearSieve()//线性筛筛phi
		{
			phi[1]=1;for(RI i=2,j,t;i<=SZ;++i)
				for(!P[i]&&(phi[P[++Pt]=i]=i-1),j=1;j<=Pt&&(t=i*P[j])<=SZ;++j)
					if(P[t]=1,i%P[j]) phi[t]=phi[i]*(P[j]-1);else {phi[t]=phi[i]*P[j];break;}
		}
}Phi;
int main()
{
	freopen("sequence.in","r",stdin); freopen("sequence.out","w",stdout);
	RI i,j,x,ans=0,Mx=0;for(F.read(n),i=1;i<=n;++i) F.read(x),Mx|=x,++p[x];//Mx统计DP转移上界
	for(ans=f[0]=i=1;i<=Mx;++i)
	{
		for(f[i]=p[i],j=i&(i-1);j;j=i&(j-1)) j>(i^j)&&(f[i]=(1LL*p[j]*f[i^j]+f[i])%X);
		//特殊处理i=j(即只有新加入的这个元素时),否则要在j>(i^j)时转移避免重复计算贡献
		ans=(1LL*Phi[i+1]*f[i]+ans)%X;//统计答案
	}
	for(i=1;i<=p[0];++i) (ans<<=1)>=X&&(ans-=X);return printf("%d",ans),0;
	//注意到0选择与否从各方面(合法性、价值)都不影响优秀子序列,所以最后给答案乘上2的p[0]次幂
}
原文地址:https://www.cnblogs.com/chenxiaoran666/p/NOIOnline3TG.html