noip模拟30 毛一琛 毛二琛 毛三琛

咕了好久了,来更一篇(

这场比赛可以说吸取了之前的教训,但没有吸取完。

考场上顺序开题。

\(\mathrm{A.}\mathbb{毛一琛}\):状压

\(\mathrm{B.}\mathbb{毛二琛}\):不知道

\(\mathrm{C.}\mathbb{毛三琛}\):不知道

可以说毫无头绪 \(\color{green}(\)

别人:

研究 \(\mathrm{A}\Rightarrow\) 拿到高分

研究 \(\mathrm{B}\Rightarrow\) 拿到高分

研究 \(\mathrm{C}\Rightarrow\) 拿到高分

或者是:

研究 \(\mathrm{A}\Rightarrow\) 研究 \(\mathrm{B}\Rightarrow\) 研究 \(\mathrm{C}\Rightarrow\) 拿到高分

我:

研究 \(\mathrm{\{A,B,C\}}\Rightarrow\) 反复横跳 \(\Rightarrow\) 时间不足 \(\Rightarrow\) 一通乱搞 \(\Rightarrow\) 搞成低分

主要是在一直考虑 \(\mathrm{A}\) 的优化和 \(\mathrm{B}\) 的假做法。

这种状况持续了两个小时(

最后的一个小时:按照 \(\mathrm{\{C,A,B\}}\) 的顺序写暴力。

估分:\(20+0+40=\color{green}60\)

实际:\(15+0+40=55\)

还是说正解吧(

\(\mathrm{A.}\mathbb{毛一琛}\)

考场上只想到了 \(O(3^n)\) 的枚举相等子集的做法,一直在想优化,但没想出来。

正解

\(\mathrm{meet}\ \mathrm{in}\ \mathrm{the}\ \mathrm{middle}\)

考虑把原来长度为 \(n\) 的序列分为两个长度为 \(\frac{n}{2}\) 的序列,先算一边,用一个 \(\mathrm{map}\),以两个子集的差为下标,把更新的状态全存进去。

再算另一边,进行相同的操作,只不过不用存进去,把相同差的所有(前半的)状态与枚举到的当前的(后半的)状态或出最终的合法的状态,用一个 \(vis\) 数组进行判断是否合法。

最终只需统计有多少个状态 \(i\) 使得 \(vis_i=1\)

时间复杂度为 \(O(6^{\frac{n}{2}}\color{green})\)

code
#include<bits/stdc++.h>
using namespace std;
const int N=21;
int n;
int a[N],sum[(1<<(N-1))+5],lg[(1<<(N-1))+5];
bool vis[(1<<(N-1))+5];
int ans;
map<int,vector<int> >q;
inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-48;
		ch=getchar();
	}
	return x*f;
}
int main()
{
	n=read();
	int base=1<<(n>>1),limit=(1<<n)-1;
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=0;i<=limit;i+=base)
	{
		for(int j=base,k=(n>>1)+1;j<=limit;j<<=1,k++)
		{
			if(i&j) sum[i]+=a[k];
		}
	}
	for(int i=0;i<=base-1;i++)
	{
		for(int j=1,k=1;j<=base-1;j<<=1,k++)
		{
			if(i&j) sum[i]+=a[k];
		}
	}
	for(int i=0;i<=limit;i+=base)
	{
		for(int j=0;j<=limit;j+=base)
		{
			if((i&j)==0) q[sum[i]-sum[j]].push_back(i|j);
		}
	}
	for(int i=0;i<=base-1;i++)
	{
		for(int j=0;j<=base-1;j++)
		{
			if((i&j)==0)
			{
				for(int k:q[sum[i]-sum[j]]) vis[k|i|j]=1;
			}
		}
	}
	for(int i=1;i<=limit;i++) ans+=vis[i];
	printf("%d\n",ans);
	return 0;
}

\(\mathrm{B.}\mathbb{毛二琛}\)

考场上想的是先求出一种合法的 \(q\),再从这个 \(q\) 去拓展其它合法的 \(q\),结果这个做法被A神叉掉了。

正解

定义 \(cmp_i\) 为在序列 \(q\) 中第 \(i\) 位与第 \(i+1\)\(\mathrm{swap}\) 的先后顺序,对于一个数,如果想回到原来的位置的话,从当前这个位置到原本的位置这一段区间,在序列 \(q\) 上的顺序就已经固定了。

\(cmp\) 的方法可以用一个右指针 \(r\),初始 \(r=n-1\),每次在 \([1,r]\) 中寻找 \(\max\) 然后计算 \(cmp\)\(\max\)\(r\) 的值,最后 \(O(n^2)\ \mathrm{dp}\) 即可。

记得处理边界。

\(f_{i,j}\) 表示 \(i\) 在前 \(i\) 个数中排名为 \(j\) 的方案数,则有:

\(f_{i,j}=\begin{cases} \displaystyle\sum_{k=j}^{i-1}f_{i-1,k}&\text{if}\ cmp_{i-1}=\color{green}1\\ \displaystyle\sum_{k=1}^{j-1}f_{i-1,k}&\text{if}\ cmp_{i-1}=0 \end{cases}\\\)

前缀和维护即可做到 \(O(n^\color{green}2)\) 的时间复杂度。

另一种求 \(cmp\) 数组的方法可以见dalao的博客

code
#include<iostream>
using namespace std;
const int N=5005,mod=1e9+7;
int n,mx,id;
int p[N],cmp[N];
int f[N][N],sum[N][N];
long long ans;
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>p[i];
		p[i]++;
	}
	int r=n-1;
	while(r>=1)
	{
		mx=0;
		for(int i=1;i<=r;i++)
		{
			if(mx<p[i]) 
			{
				mx=p[i];
				id=i;
			}
		}
		for(int i=id;i<=r-1;i++) cmp[i]=1;
		cmp[id-1]=0;
		if(mx>p[r+1]) cmp[r]=1;
		r=id-2;
	}
	f[1][1]=sum[1][1]=1;
	for(int i=2;i<=n-1;i++)
	{
		for(int j=1;j<=i;j++)
		{
			if(cmp[i-1]==1) f[i][j]=(sum[i-1][i-1]-sum[i-1][j-1]+mod)%mod;
			else f[i][j]=sum[i-1][j-1];
		}
		for(int j=1;j<=i;j++) sum[i][j]=(sum[i][j-1]+f[i][j])%mod;
	}
	cout<<sum[n-1][n-1]<<endl;
	return 0;
}

\(\mathrm{C.}\mathbb{毛三琛}\)

正常的枚举加二分,复杂度为 \(O(nP\log nP)\)

正解

在二分的基础上加上随机化,去掉枚举 \(x\) 的部分,改成用 \(\mathrm{random\_shuffle}\) 来随机化的选取 \(x\),如果当前 \(x\) 可以使得 \(ans\) 更优,再进行二分,否则直接 \(continue\),一个随机排列中比前面所有数都大的数的数量期望为 \(\log\)(感性理解),因此复杂度为\(O(nP+n\log n\log P)\)

code
#include<bits/stdc++.h>
using namespace std;
const int N=10005;
int n,P,k;
int a[N],b[N],c[N];
int ans=0x3f3f3f3f;
bool check(int x)
{
	int cnt=1,sum=0;
	for(int i=1;i<=n;i++)
	{
		sum+=b[i];
		if(b[i]>x) return 0;
		if(sum>x) cnt++,sum=b[i];
	}
	if(cnt<=k) return 1;
	return 0;
}
int main()
{
	srand(time(0));
	cin>>n>>P>>k;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=0;i<=P-1;i++) c[i]=i;
	random_shuffle(c,c+P); 
	for(int j=0;j<=P-1;j++)
	{
		int x=c[j];
		for(int i=1;i<=n;i++) b[i]=(a[i]+x)%P;
		if(check(ans)==0) continue;
		int l=0,r=P*n;
		while(l<r)
		{
			int mid=(l+r)/2;
			if(check(mid)) r=mid;
			else l=mid+1;
		}
		ans=min(ans,l);
	}
	cout<<ans<<endl;
	return 0;
}