高一下三调|ssy的队列|hash dp|题解

image
image
SSY的队列题目链接

解析:

考场上看到这个题第一眼是绝望的,毕竟数论咱是一窍不通.
但是往下细看了看这个数据范围,N是很小的,就想了想模拟.
然而只骗到10分.
这个题绩效较高的解法是状压dp,在N<15的范围之内均可薄纱,可得70(ppllxx_9G也是成功拿到这70 rank 1了 orz),但是一到后面三个点就是RE了.
虽说非正解,但也是借这个题又回顾了一下我们的状压dp.
状压代码:

#include<bits/stdc++.h>
#define ll long long
#define qr qr()
using namespace std;

inline ll qr
{
	ll x=0;char ch=getchar();bool f=1;
	while(ch>57||ch<48)
	{
		if(ch=='-')f=0;
		ch=getchar();
	}
	while(ch>=48&&ch<=57)x=(x<<3)+(x<<1)+ch-48,ch=getchar();
	if(f)return x;
	else return ~(x-1);
}
const int mod=1234567891;
ll n,m,num[31],f[1<<17][17],ans;
bool vis[17][17];
void init()
{
	n=qr;
	for(int i=1;i<=n;++i)
		num[i]=qr;
	putchar('\n');
	m=qr;
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<i;++j)
		{
			if(abs(num[i]-num[j])%m)vis[i][j]=vis[j][i]=1;
		}
	}
	for(int i=0;i<n;++i)
		f[(1<<i)][i]=1;
	for(int i=1;i<(1<<n);++i)//复杂度 O((n^2)*(2^n))
	{//状压枚举状态 我们每次都是在整个状态的末尾加新的人 
		for(int now=0;now<n;++now)
		{//枚举站在最末尾的人 
			if((1<<now)&i)
			{//这个人在状态内 
				for(int man=0;man<n;++man)
				{//枚举新加入的人 
					if(((1<<man)&i));
					else if(vis[man+1][now+1])
						f[i|(1<<man)][man]=(f[i|(1<<man)][man]+f[i][now])%mod;
				}
			}
		}
	} 
	for(int i=0;i<n;++i)
		ans=(ans+f[(1<<n)-1][i])%mod;
	printf("%lld",ans);
}
int main()
{
	freopen("ssy.in","r",stdin);
	freopen("ssy.out","w",stdout); 
	init();
	return 0;
} 

正解思路十分奇妙,我们观察题目的描述,意思是所有对M余数相等的数是不可放在一起的.
那么我们可以将所有的数以余数分类,再进行处理.
可知:
1.同类数不可以放在一起,但是同类数的位置均可对调.
2.我们定义的某个状态的种类数,事实取决于某个状态不同种类的不同剩余个数的情况.
第二条是较难归纳出的,我们来看一个浅显的例子:
2233这四个数进行排列的种类数与4455这四个数排列的情况数是一致的.
状压为什么会炸,因为它存了很多没有用的状态而且也访问并转移了许多没有用的状态.
同时因为它没有类别之分,就会导致很多时候它的一些状态是已经访问过的,但状压无法辨识.
如此看状压已经没有优化空间,但是状压的思路我们要留着.
接着看如何实现.
我们根据上述规律去走一遍dfs,只不过这次不是单纯的暴力模拟了,我们加入了记搜,状压,以及hash成分.
为什么还有hash呢,因为我们想想,dfs依旧以着dp的思路在跑.
然而我们的dp描述状态要各个数余下的种类以及最后一个数是哪一种的(因为明显我们是不可以把两个同种的放在一起的).
但是它有三十个数,所以明显咱们不能开三十维.
那就要借助一个hash的思想,将状态压成一个ull的数,同时用map将这个数映射到一个可以操作的下标上.
这样就能保证转移有效,省去空间,同时得到正解.

正解:

#include<bits/stdc++.h>
#define ll long long
#define llu unsigned long long
using namespace std;

const int N=50,mod=1234567891,mode=131;
//        种类数↓  ↓每个种类中所含数. 
int n,num[N],m,tot,all[N],nowleft[N],mx;
//                           ↑未被操作的剩下个数为相应大小的种类的个数. 
bool vis[N];
map <llu,ll> mp[N];//用map实现记忆化,保证重复状态不重搜. 
ll ans,pw[N];
//                 ↓最后一个数是哪一种的. 
ll dfs(int now,int lst)
{//        ↑填到第几个数 
	if(now>n)//边界是所有数都已填入. 
		return 1;
	memset(nowleft,0,sizeof(nowleft));
	for(int i=1;i<=tot;++i)
		if(i^lst)++nowleft[all[i]];
	llu nzt=all[0];
	for(int i=1;i<=mx;++i)//个数为0的种类不必存入状态中. 
		nzt=nzt*mode+nowleft[i];
	nzt=nzt*mode+all[lst]; 
	if(mp[now].find(nzt)!=mp[now].end())
		return mp[now][nzt];//已经访问过的状态不再搜. 
	ll nans=0;
	if(all[0])
	{
		--all[0];
		nans=(nans+dfs(now+1,0))%mod;
		++all[0];
	}
	for(int i=1;i<=tot;++i)
	{
		if((i^lst)&&all[i])
		{
			--all[i];
			nans=(nans+dfs(now+1,i))%mod;
			++all[i];
		}
	}
	return mp[now][nzt]=nans;//记忆化 
}
void init()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
	{
		scanf("%d",&num[i]);
	}
	scanf("%d",&m);
	for(int i=1;i<=n;++i)
		num[i]=(num[i]%m+m)%m;
	for(int i=1;i<=n;++i)
	{
		if(vis[i])continue;
		vis[i]=1;
		int cnt=0;
		for(int j=i;j<=n;++j)
		{
			if(num[i]^num[j]);
			else vis[j]=1,++cnt;
		}
		mx=max(mx,cnt);
		if(cnt==1)++all[0];
		else all[++tot]=cnt;
	}
	pw[0]=1;
	mx=max(all[0],mx);
	for(int i=1;i<=mx;++i)
		pw[i]=pw[i-1]*i%mod;
	ans=1;
	for(int i=0;i<=tot;++i)
		ans=ans*pw[all[i]]%mod;
	ans=ans*dfs(1,0)%mod;
	printf("%lld",ans);
}
int main()
{
	freopen("ssy.in","r",stdin);
	freopen("ssy.out","w",stdout); 
	init();
	return 0;
}
posted @ 2024-05-05 20:26  SLS-wwppcc  阅读(84)  评论(0编辑  收藏  举报