HGOI11.2集训题解

题解

今天这个题其实都是乱搞的…但最后一道题暴力分没拿好(没过滤前导零),有点可惜
LJ老师说得对,你可以不会很多算法,但你要会考试


第一题——集合划分(partition)

【题目描述】

  • 给定一个包含 N N N个非负整数的集合 A A A,请将 A A A分成两个子集 P , Q P,Q P,Q,使得 P ∩ Q = ∅ P\cap Q=\varnothing PQ=,且 g c d ( ∏ P i , ∏ Q i ) gcd(\prod{P_i},\prod{Q_i}) gcd(Pi,Qi)。请计算这样的划分方法总数是多少。由于答案可能很大,请mod 1000000007 后输出。

  • 这个题刚刚拿到题比较难想,大概磨了半个小时的样子,从数据当中找出些规律:
  • 发现对于一个待选元素,归入的集合当中的元素不一定和他有公有因数,但另外一个集合一定和他互质
  • 那么考虑对于一个非质数的数,它将若干个包含的质数联系起来,凡是含有这些质数的元素,必须放在同一个集合当中。
  • 那么就可以利用欧拉筛筛素数,然后并查集解决被非素数联系起来的集合问题,然后求出集合个数,然后就是组合题了!(其实不用组合orz)

#include <bits/stdc++.h>
#define LL long long
using namespace std;
void fff(){
	freopen("partition.in","r",stdin);
	freopen("partition.out","w",stdout);
}
int read(){
	int x=0;char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
	return x;
}
const int N=1e5+10;
const int MOD=1e9+7;
int n;
int prime[N],cnt=0;
bool visited[N*10];
int a[N],fa[N];
LL p[N],q[N];
int num=0;
void oula(){
	for(int i=2;i<N*10;i++){
		if(!visited[i])
			prime[++cnt]=i;
		for(int j=1;j<=cnt&&prime[j]*i<N*10;j++){
			visited[prime[j]*i]=true;
			if(i%prime[j]==0) break;
		}
	}
	p[0]=p[1]=q[1]=q[0]=1;
	for(int i=1;i<N;i++)p[i]=p[i-1]*i%MOD;
	for(int i=2;i<N;i++)q[i]=(MOD-MOD/i)*q[MOD%i]%MOD;
	for(int i=2;i<N;i++)q[i]=q[i]*q[i-1]%MOD;
}
int find_(int x){
	return x==fa[x]?fa[x]:fa[x]=find_(fa[x]);
}
LL C(LL n,LL m){
	return p[n]*q[m]%MOD*q[n-m]%MOD;
}
int main(){
	fff();
	int T;T=read();
	oula();
	while(T--){
		memset(visited,false,sizeof(visited));
		n=read();num=0;
		for(int i=1;i<=cnt;i++)fa[i]=i;
		for(int i=1;i<=n;i++){
			a[i]=read();
			if(a[i]==1){
				num++;
				continue;
			}
			int temp=-1;
			int t=a[i];
			for(int j=1;j<=cnt&&prime[j]*prime[j]<=a[i];j++){
				if(a[i]%prime[j]==0){
					visited[j]=true;
					if(temp==-1){
						temp=j;
					}else{
						int x=find_(temp),y=find_(j);
						fa[y]=x;
						temp=j;
					}
					while(t%prime[j]==0)
						t/=prime[j];
				}
				if(t==1) break;
			}
			if(t>1){
				int pos=lower_bound(prime+1,prime+cnt+1,t)-prime;
				if(temp==-1){
					visited[pos]=true;
				}else{
					int x=find_(temp),y=find_(pos);
					fa[y]=x;
				}
			}
		}
		for(int i=1;i<=cnt;i++) if(visited[i]&&fa[i]==i) num++;
		LL ans=0;
		for(int i=1;i<num;i++){
			ans=(ans+C(num,i))%MOD;
		}
		printf("%lld\n",ans);
	}
}

第二题——大联欢(party)

【题目描述】

  • 给出n个点m条边,要求分成两个图满足两个图当中的点与所在图的每一个节点都有连边。若无法建成输出-1。

  • 这个二分图难道不是写在脸上了么????
  • 果断取反图,不联系的建立二分图染色,分成两个集合。
  • 有一部分和所有的节点都有连边,那就进行背包处理。这个处理比较简单,不赘述

#include <bits/stdc++.h>
using namespace std;
void fff(){
	freopen("party.in","r",stdin);
	freopen("party.out","w",stdout);
}
int read(){
	int x=0;char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
	return x;
}
const int N=710;
bool mp[N][N];
int val[N],cnt=0;
int f[N][2];
int n,m;
int d[N];
int main(){
	fff();
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read();
		mp[u][v]=mp[v][u]=true;
		d[u]++,d[v]++;
	}
	memset(val,-1,sizeof(val));
	for(int i=1;i<=n;i++){
		if(val[i]==-1) val[i]=1;
		for(int j=1;j<=n;j++){
			if(i==j)continue;
			if(!mp[i][j]){
				if(val[j]==-1) val[j]=1-val[i];
				else if(val[i]==val[j]){
					cout<<-1;
					return 0;
				}
			}
		}
	}
	int num[2];num[1]=num[0]=0;
	for(int i=1;i<=n;i++){
		if(d[i]<n-1){
			num[val[i]]++;
		}else{
			cnt++;
		}
	}
	f[0][0]=num[0],f[0][1]=num[1];//num[0]'number
	for(int i=1;i<=cnt;i++){
		if((f[i-1][0]+1)*(f[i-1][0])+(i+num[1]+num[0]-f[i-1][0]-1)*(i+num[1]+num[0]-f[i-1][0]-2)<
			(f[i-1][0])*(f[i-1][0]-1)+(i+num[1]+num[0]-f[i-1][0])*(i+num[1]+num[0]-f[i-1][0]-1)){
				f[i][0]=f[i-1][0]+1;
				f[i][1]=f[i-1][1];
		}else{
			f[i][1]=f[i-1][1]+1;
			f[i][0]=f[i-1][0];
		}
	}
	cout<<f[cnt][0]*(f[cnt][0]-1)/2+f[cnt][1]*(f[cnt][1]-1)/2;
}

第三题——密码(passwd)

【题目描述】

  • 给出一个由0…9组成的至多为17位的数字,要求有这个数的各个位数进行排列,求出字典序第K小的能被17整除的数。

  • 这个暴力好像只有30分,但作为第三题你乱搞也搞不出什么东西还是算了吧。
  • 预处理就是对读进来的数进行一个排序,主要是为了防止重复出现的排列。
  • 考虑状压,令 i i i表示当前选择哪些数字的状态, w w w表示除以 17 17 17之后的取模的余数是多少, j j j表示当前在试填第 j j j位。
  • f [ i ] [ w ] f[i][w] f[i][w]表示 i i i状态下的余数为 w w w的后面有多少个数可以选。
  • 那么就是可以写出方程
	if(!(i&pp[j]))	f[i|pp[j]][(l+a[j]*w%MOD)%MOD]+=f[i][l];
	//pp是预先处理好的二进制位数(这里二进制位移次数较多,常数较大)
	//l表示当前弄出来的余数
	//f[i|pp[j]][(l+a[j]*w%MOD)%MOD]是转移的目标状态,a[j]表示选第j个数。
  • 大概就是和数位dp差不多的做法,但是还有一个就是要处理重复的数字。
  • 这个处理效果就是对于每一个状态 i i i来说,记录有多少个重复的数字,然后在当前状态下除以数字个数的阶乘就可以了
  • 然后就是常规的数位/计数dp做法,试填法,从 i = ( 1 &lt; &lt; 17 ) − 1 i=(1&lt;&lt;17)-1 i=(1<<17)1(最大的状态)开始做起,枚举位数和当前位上锁选择的数。
  • 如果当前这个和上一个选过的一样,那就过滤(处理重复),如果当前状态下的数要比你找的k要来的大,那就输出当前位置上的当前数,否则k减去当前这个状态的这个数。
  • 最后补位输出。

#include <bits/stdc++.h>
#define LL long long
using namespace std;
void fff(){
	freopen("passwd.in","r",stdin);
	freopen("passwd.out","w",stdout);
}
const int MOD=17;
bool flag=false;
char s[20];
LL n;
LL fact[20],f[1<<17][18];
int num[10];
int a[20];
LL pp[18]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768,65536,131072};
LL p10[18];
LL k;
int main(){
	fff();
	scanf("%s",&s);
	scanf("%lld",&k);
	int len=strlen(s)-1;
	for(int i=0;i<strlen(s);i++){
		a[i]=s[i]-'0';
	}
	sort(a,a+strlen(s));
	n=(1<<(strlen(s)))-1;
	f[0][0]=1;
	for(int i=0;i<n;i++){
		int w=1;
		for(int j=0;j<=len;j++) if(i&pp[j]) w=w*10%MOD;
		for(int j=0;j<=len;j++)
			for(int l=0;l<=16;l++)
				if(!(i&pp[j]))	f[i|pp[j]][(l+a[j]*w%MOD)%MOD]+=f[i][l];
	}
	fact[0]=1;
	for(int i=1;i<=17;i++) fact[i]=fact[i-1]*i;
	for(int i=0;i<n;i++){
		int c[10];
		memset(c,0,sizeof(c));
		for(int j=0;j<=len;j++)if(i&pp[j])c[a[j]]++;
		for(int j=0;j<=9;j++) for(int l=0;l<=16;l++)f[i][l]/=fact[c[j]];
	}
	int s=n,p=0;
	p10[0]=1;
	for(int i=1;i<=17;i++) p10[i]=p10[i-1]*10%MOD;
	for(int i=0;i<=len;i++){
		int last=-1;
		for(int j=0;j<=len;j++)
			if(s&pp[j]){
				if(a[j]==last) continue;
				last=a[j];
				if(!i&&!a[j])continue;
				if(f[s^pp[j]][(17-(p+a[j]*p10[len-i])%MOD)%MOD]>=k){
					s^=pp[j];
					p=(p+a[j]*p10[len-i])%MOD;
					printf("%d",a[j]);
					break;
				}else k-=f[s^pp[j]][(17-(p+a[j]*p10[len-i])%MOD)%MOD];
			}
	}
	for(int i=0;i<=len;i++)if(s&pp[i]) printf("%d",a[i]);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值