NOI Online题解

愤怒的小N

题目描述

点此看题

解法

首先可以发现奖励关就是二进制 \(1\) 个数为奇数的数。

先讲一下 \(60\) 分的做法,因为并不是人人一来就能拿满分,但这是正解的一个引子。

看到这个限制就想到了用数位 \(dp\) 去做,我们从小数位往大数位考虑,那么我们尝试维护 \(x^t\) 的和,每当遇到一个 1 之后就统计答案,不难发现可以固定大数位一定,这一位设置为 0,然后小数位随便选,就可以统计出所有的数。

设 \(dp[t][0/1]\) 表示考虑了后 \(n-i\) 个小数位(我把这一维滚动掉了),\(1\) 的个数是偶数\(/\)奇数的 \(x^t\) 的和。因为这个状态的含义是乱填,转移就考虑这一位是填 \(0\) 还是 \(1\),原理就用二项式展开(其中 \(pre\) 表示之前的数):

\[(2^{n-i}+pre)^t=\sum_{l=0}^t pre^l\cdot 2^{(n-i)l}\cdot{t\choose l} \]

那么我们就根据这个来转移:

  • 这一位填 \(1\),\(dp[t][0]\leftarrow \sum_{l=0}^t 2^{(n-i)l}\cdot dp'[t-l][1]\cdot{t\choose l}\),\(dp[t][1]\leftarrow \sum_{l=0}^t2^{(n-i)l}\cdot dp'[t-l][0]\cdot{t\choose l}\)

  • 这一位填 \(0\),\(dp[t][0]\leftarrow dp'[t][0]\),\(dp[t][1]\leftarrow dp'[t][1]\)

初始令 \(dp[0][0]=1\),算答案的时候也用二项式定理把前后合并起来即可,时间复杂度 \(O(k^2\log_2n)\),\(60\) 分到手。

然鹅我在考试的时候写错了一个小地方,调了很久,在我输出第二组样例的中间变量是发现了下面的神奇东西。

为了版面我就不贴过来了,点此观察数据(附暴力代码)

发现每迭代一次就会多一个 \(dp[t][0]\) 和 \(dp[t][1]\) 相等,迭代 \(k\) 次之后都满足 \(dp[t][0]=dp[t][1]\),我还以为是我暴力 \(dp\) 打错了,但是手玩了一下发现没问题,年轻的我不知道这就是正解的引子。


现在给出本题至关重要的结论,当迭代次数(\(dp\) 了多少次)大于 \(k\) 时,都有 \(dp[t][0]=dp[t][1]\),知道了这个结论可以归纳证明它:

当迭代次数为 \(1\) 时有 \(dp[0][0]=dp[0][1]=1\),当迭代次数为 \(x\) 的时候 \(dp'[x-1][0]+dp'[x-1][1]\) 会贡献到 \(dp[x-1][0],dp[x-1][1]\) 那里去,那么对于 \(x-1\) 这个次数的贡献是相等的,而比 \(x-1\) 更小的项由于数值相等的贡献是一样的,所以贡献相等,那么就多了 \(x-1\) 这一项的相等。

总结一句:秘密就藏在转移式里面。

那么也就是说迭代次数大于 \(k\) 之后答案就和 \(1\) 出现次数是奇是偶没关系了,直接算所有数除以 \(2\) 就得到奇数的答案了,那么可以改变一下算答案的方式,我们先统计出全部的数量:

\[\frac{1}{2}\sum_{i=0}^{n-1}f(i)=\frac{1}{2}\sum_{t=0}^{k-1}a_t\sum_{i=0}^{n-1}i^t \]

\(\sum_{i=0}^{n-1}i^t\) 这个东西可以套路地算出来,就让 \(\tt yyb\) 教你算吧:点此学,这部分时间复杂度 \(O(\log_2 n+k^2)\)

然后还要用迭代次数小于等于 \(k\) 的答案去修正他,设 \(ji\) 表示 \(1\) 出现次数为奇数的答案,\(ou\) 表示 \(1\) 出现次数为偶数的答案,所以最终的答案就是:

\[\frac{1}{2}(\sum_{i=0}^{n-1}+ji-ou) \]

然后 \(ji,ou\) 这两个东西可以用上面讲的暴力的做法 \(O(k^3)\) 算出来,总时间复杂度 \(O(\log_2 n+k^3)\)

积木小赛

题目描述

点此看题

解法

唯一的教训是 \(\tt map\) 常数太大了,改成排序去重就过了。

时间复杂度 \(O(n^2\log n)\),考试的时候应该多关注一下常数问题吧。

#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 3005;
#define ull unsigned long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,ans,dp[M][26];char s[M],t[M];
ull hs[M],pw[M],a[M*M];
ull get(int l,int r)
{
	return hs[r]-hs[l-1]*pw[r-l+1];
}
int main()
{
	n=read();
	scanf("%s",s+1);scanf("%s",t+1);
	//搞一点预处理
	pw[0]=1;
	for(int i=1;i<=n;i++)
	{
		pw[i]=pw[i-1]*391;
		hs[i]=hs[i-1]*391+t[i];
	}
	for(int j=0;j<26;j++) dp[n+1][j]=n+1;
	for(int i=n;i>=1;i--)
	{
		for(int j=0;j<26;j++)
			dp[i][j]=dp[i+1][j];
		dp[i][s[i]-'a']=i;
	}
	//枚举t的所有子串
	for(int l=1;l<=n;l++)
	{
		int p=1;
		for(int r=l;r<=n;r++)
		{
			//匹配不动了
			if(dp[p][t[r]-'a']==n+1) break;
			p=dp[p][t[r]-'a']+1;
			a[++ans]=get(l,r);
		}
	}
	sort(a+1,a+1+ans);
	ans=unique(a+1,a+1+ans)-a-1;
	printf("%d\n",ans);
}

岛屿探险

题目描述

点此看题

解法

一开始写了一个树套树,虽然时间复杂度 \(O(n\log^2n)\) 但还是直接挂掉了,这东西又耗空间常数又大。

我们来分析一下要求的柿子:\(a_i\oplus c_j\leq \min(b_i,d_j)\),看到 \(\min\) 按照套路把它拆开。

如果要把他拆开就会产生一个关于 \(b_i\) 和 \(d_j\) 的偏序关系,解决偏序关系可以用 \(\tt cdq\) 分治,这个算法的套路是先想到分治再去想怎么套,所以我们把询问和修改塞到一起拿去 \(\tt cdq\),外层按 \(b_i,d_j\) 排序即可。

对于 \(b_i\leq d_j\) 的情况,问题转化成了 \(a_i\oplus c_j\leq b_i\),这就是左半边的修改对右半边询问的贡献,异或问题可以考虑 \(\tt tire\) 来解决,那么我们考虑一颗关于 \(c_j\) 的树,用 \(a_i,b_i\) 再上面跑的时候直接打永久化标记就行了,统计的时候就统计链上被打上的标记之和。

对于 \(b_i>d_j\) 的情况,问题转化成了 \(a_i\oplus c_j>d_j\),这就是右半边的修改对左半边询问的贡献,考虑一颗关于 \(a_i\) 的树,建出来之后把 \(c_j,d_j\) 在 \(\tt tire\) 树上面跑就行了。

然后左半边和右半边都按位置排序即可,询问拆成 \([1,l),[1,r]\) 这两个会方便一些。还有一个细节是 \(b_i=d_j\) 的情况不用关心,归类到哪一边算都可以,直接排序就行。时间复杂度 \(O(n\log^2n)\)

再写树套树我吃屎

#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 300005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,q,cnt,rt,ans[M],ls[50*M],rs[50*M],s[50*M];
struct node
{
	int x,c,d,f;
}a[M];
bool cmp1(node x,node y)
{
	return x.d<y.d;
}
bool cmp2(node x,node y)
{
	return x.x<y.x;
}
void init()//初始化 
{
	for(int i=1;i<=cnt;i++)
		ls[i]=rs[i]=s[i]=0;
	rt=cnt=0;
}
//subtask1
void ins1(int &x,int y,int p)
{
	if(!x) x=++cnt;
	s[x]++;
	if(p==-1) return ;
	if(y&(1<<p)) ins1(rs[x],y,p-1);
	else ins1(ls[x],y,p-1);
}
int ask1(int x,int c,int d,int p)
{
	if(!x || p==-1) return s[x];
	if(d&(1<<p))
	{
		if(c&(1<<p)) return ask1(ls[x],c,d,p-1)+s[rs[x]];
		return ask1(rs[x],c,d,p-1)+s[ls[x]];
	}
	if(c&(1<<p)) return ask1(rs[x],c,d,p-1);
	return ask1(ls[x],c,d,p-1);
}
//subtask2
void tag(int &x)
{
	if(!x) x=++cnt;
	s[x]++;
}
void ins2(int &x,int c,int d,int p)
{
	if(!x) x=++cnt;
	if(p==-1) {tag(x);return ;}
	if(d&(1<<p))
	{
		if(c&(1<<p)) {tag(rs[x]);ins2(ls[x],c,d,p-1);}
		else {tag(ls[x]);ins2(rs[x],c,d,p-1);}
		return ;
	}
	if(c&(1<<p)) ins2(rs[x],c,d,p-1);
	else ins2(ls[x],c,d,p-1);
}
int ask2(int x,int c,int p)
{
	int t=s[x];
	if(!x || p==-1) return t;
	if(c&(1<<p)) return t+ask2(rs[x],c,p-1);
	return t+ask2(ls[x],c,p-1);
}
void cdq(int l,int r)
{
	if(l==r) return ;
	int mid=(l+r)>>1;
	cdq(l,mid);cdq(mid+1,r);
	sort(a+l,a+mid+1,cmp2);
	sort(a+mid+1,a+r+1,cmp2);
	//subtask1:右边建tire左边查
	init();
	for(int i=l,j=mid+1;i<=mid;i++)
		if(a[i].f!=0)//是询问 
		{
			while(j<=r && a[j].x<=a[i].x)
			{
				if(a[j].f==0) ins1(rt,a[j].c,23);
				j++;
			}
			if(a[i].f>0) ans[a[i].f]+=ask1(rt,a[i].c,a[i].d,23);
			else ans[-a[i].f]-=ask1(rt,a[i].c,a[i].d,23); 
		}
	//subtask2:左边打标记右边查
	init();
	for(int i=mid+1,j=l;i<=r;i++)
		if(a[i].f!=0)
		{
			while(j<=mid && a[j].x<=a[i].x)
			{
				if(a[j].f==0) ins2(rt,a[j].c,a[j].d,23);
				j++;
			}
			if(a[i].f>0) ans[a[i].f]+=ask2(rt,a[i].c,23);
			else ans[-a[i].f]-=ask2(rt,a[i].c,23);
		}
}
int main()
{
	n=read();q=read();
	for(int i=1;i<=n;i++)
	{
		int c=read(),d=read();
		a[++m]=node{i,c,d,0};
	}
	for(int i=1;i<=q;i++)
	{
		int l=read(),r=read(),c=read(),d=read();
		a[++m]=node{r,c,d,i};
		a[++m]=node{l-1,c,d,-i};
	}
	sort(a+1,a+1+m,cmp1);
	cdq(1,m);
	for(int i=1;i<=q;i++)
		printf("%d\n",ans[i]);
}
上一篇:MySQL5.7新特性之在线DDL不会锁表


下一篇:杂题题解 3