本周难题总结

对于本周模拟赛的难题进行分析
DAY1

【问题描述】
你是一位精明的逻辑学家,擅长用计算机来解决复杂的逻辑学问题。
在逻辑学推断中,你得到两个 01 数列,长度分别为 29n 和 29m。为了
方便进行数学表达,你将这两个数列转化为长度分别为 n 和 m 的数列,每
一个数字代表一个长度为 2901 串(二进制表达)。记这两个数列为 {Vi}{Ui}。
由这两个数列可以得到矩阵 A,满足 Ai,j = Vi xor Uj。
你希望求得 A 的一个子矩阵使得其内部异或和最大。
【输入格式】
第一行为两个正整数 n,m,用空格隔开。
第二行为 n 个非负整数,表示 {Vi}。
第三行为 m 个非负整数,表示 {Ui}。
n <= 1000 vi,ui<= 2^29

首先我们考虑一下暴力怎么写。我们可以枚举每个矩阵的长度和宽度,然后再枚举这个矩阵的位置,那么也就是,四次方的复杂度。这样的做法可以在考试中拿到30分。

下面我们来考虑一下怎么优化这种做法。我们可以把矩阵的每一个元素都先写成vi * uj的形式 又因为异或的性质 :x^x = 0 , 所以我们可以发现当一个矩阵的长和宽都是偶数时,这个矩阵其实他的异或和一定为0 ; (自己手动模拟可得)可惜我考试的时候没发现
所以 对答案有贡献的只有两种情况:一边为奇一边为偶 或者两边都为奇

当一边为奇一边为偶数的时候, 答案为长度为奇的那一段的异或和 ;
对于这种情况 枚举区间即可 n^2的复杂度显然可过

而当两边都为奇的时候,就是任意两段长度为奇的数异或在一起
如果照旧枚举的话 可能复杂度会到n^3 不符合要求

我们来考率当一段异或和为1011的时候,我们的目标是不是看另外一边是否可以为0100 因为这样答案最优对吧
这样的一个过程像什么呢?

字符串匹配!!!!!

这个时候我们就可以用二进制表示每个数,建立tried树;
查找的时候就可以在tried树上找一遍就好啦
而且ui和vi小于2^29的目的就是确保点不会太多

下面给出具体的代码, 具体的细节请读者自行思考

#include<bits/stdc++.h>
using namespace std ;
#define maxn 1010
#define int long long
#define kkk signed main
#define re register int 
inline int read(){
	int ans = 0 , f = 1 ; char ch = getchar() ;
	while ( ch < '0' || ch > '9') {	 if( ch == '-' )  f = -1 ; ch = getchar() ; }
	while ( ch >= '0' && ch <= '9' ) ans =  ( ans << 3 ) +  ( ans << 1 ) + ch - '0' , ch = getchar() ;
	return ans * f ;
}
int n , m; 
int a[maxn] , b[maxn] ; 
int ch[14000000][2] , sz; 
inline void isert(int x){
//	printf("x : %lld\n" , x) ; 
	int u = 0 ; 
	for(int i = 29 ; i >= 1 ; --i){
		int now = (x >> (i - 1)) & 1 ; 
//		printf("now : %lld i :%lld\n" , now , i) ; 
		if(!ch[u][now]){
			ch[u][now] = ++sz ; 
		} 
		u = ch[u][now] ;
	}
}
inline int query(int x){
	int u = 0  , ans = 0 ; 
	for(int i = 29 ; i >= 1 ; i--){
		int now = (x >> (i - 1)) & 1 ; 
		if(ch[u][!now]){
			ans |= (1 << (i - 1)) ;
			u = ch[u][!now];
		}
		else {
//			ans |= (now) << (i - 1) ; 
			u = ch[u][now] ;
		}
	}
	return ans ; 
}
kkk(){
	freopen("c.in" , "r" , stdin) ; 
	freopen("c.out" , "w" , stdout) ; 
	n = read() , m = read() ; 
	for(re i = 1 ; i <= n ; ++i)
		a[i] = read() ; 
	for(re i = 1 ; i <= m ; ++i)
		b[i] = read() ; 
	int ans = 0 ; 
	for(int len = 1 ; len <= n ; len++){
		for(int i = 1 ; i + len -1 <= n ; i++){
			int temp  = 0 ; 
			for(int j = i ; j <= i + len - 1 ; j++){
				temp ^= a[j] ; 
			}
			if(len % 2 && len != 1)ans = max(ans , temp) ;  
			if(len % 2)isert(temp) ;  
		}	
	}	 
	for(int len = 1 ; len <= m ; len++){
		for(int i = 1 ; i + len -1 <= m ; i++){
			int temp = 0 ; 
			for(int j = i ; j <= i + len - 1 ; j++){
				temp ^= b[j] ; 
			}
			if(len % 2 && len != 1)ans = max(ans , temp) ;
			if(len % 2)ans = max(query(temp) , ans) ; 
		}	
	}	
	printf("%lld\n" , ans) ;
}

DAY2

你是一位精明的数学家,擅长用计算机来解决各类小学奥数题。
这一天,你想起来了著名的 “开关灯问题”,它曾经经常被拿来为难小朋
友们。于是你决定完成一个程序来彻底解决这个问题。
“开关灯问题” 在小学奥数书上的描述是这样的:
一个走廊里有 100 盏按顺序排好的灯,它们分别拥有 1100 的编号,
最开始时都是关上的。小明同学非常无聊,他首先按下了所有编号为 2 的倍
数的灯的开关,接着他又按下了所有编号为 3 的倍数的灯的开关,问最后有
多少灯是打开的。
因为你希望最终完全解决这类问题,因此你将题目一般化:
现在有两种灯的开关:第一种开关是熔断型的,即只要第一次使用开关
后,开关将保持打开状态不变;第二种开关是普通型的,即使用奇数次开关
时,灯会打开,使用偶数次时,灯又会关闭。
一个走廊里有 n 盏按顺序排好的灯,它们使用的都是第 type 种开关
(type = 12)。最开始时,1 ∼ n 号灯都是关上的,开关也从未使用过。小
明同学非常无聊,他进行了 d 次操作,每一次操作形如:“将编号为 ki 的倍
数的灯的开关按下”。问经过这 d 次操作,最终有多少盏灯是打开的。
你完成的程序需要实现 Q 次任务,每一次任务会给出 n,type,d,{ki}。
【输入格式】
第一行为一个正整数 Q,表示有 Q 次任务。
对于每一次任务,第一行为三个正整数 n, type, d,第二行为 d 个正整数,
表示 ki。
【样例输入 12
6 1 2
2 3
6 2 2
2 3
【样例输出 14
3
T <= 1000 n <= 10^18 d <= 12

对于type == 1 的情况经典容斥原理 直接套结论
首先逐一枚举子集,得到子集的最小公倍数,那么它的倍数个
数就确定 ,如果子集中有奇数个元素对应加上,偶数个元素对应减去
那么对于 type == 2 的 情况则需要推式子了
经过手动模拟, 可以得出这时候的容斥系数为(-2)^(i - 1)
如果真的推的话要二项式反演,蒟蒻不会就只能模拟了
所以主要就是把容斥系数改下,和之前没有区别
本蒟蒻没想到则是因为根本就没用过容斥原理。。。。。。

#include<bits/stdc++.h>
using namespace std ;
//#define maxn
#define int long long
#define kkk signed main
#define re register int 
inline int read(){
	int ans = 0 , f = 1 ; char ch = getchar() ;
	while ( ch < '0' || ch > '9') {	 if( ch == '-' )  f = -1 ; ch = getchar() ; }
	while ( ch >= '0' && ch <= '9' ) ans =  ( ans << 3 ) +  ( ans << 1 ) + ch - '0' , ch = getchar() ;
	return ans * f ;
}
inline int gcd(int a , int b){
	return b ? gcd(b , a % b) : a ; 
}
inline int lcm(int a ,int b){
	return a * b / gcd(a , b) ; 
}
int q , n ,type , d ;
int k[13] ; 
int temp ,sum , ans;
int ksm(int a  , int b){
	int ss = 1 ; 
	while(b > 0){
		if(b & 1) ss *= a ; 
		a = a * a  ; 
		b >>= 1 ; 
	}
	return ss  ; 
}
int l[1 << 13] , size[1 << 13]; 
kkk(){
	freopen("c.in" , "r" , stdin) ; 
	freopen("c.out" , "w" , stdout) ; 
	q = read() ; 
	for(int i = 1 ; i < (1 << 12) ; i++){
		re now = i , cntt = 0 ; 
		while(now > 0){
			if(now & 1)  cntt++ ; 
			now >>= 1 ;
		}
		size[i] = cntt ; 
	}
	while(q--){
		n = read() , type = read() , d = read() ; ans = 0 ; 
		memset(k , 0 , sizeof(k)) ; memset(l , 0 , sizeof(l)) ; 
		for(re i = 1 ; i <= d ; ++i)
		k[i] = read() ; 
		int ii ; 
		l[0] = 1 ;
		for(int s = 1 ; s < (1 << d) ; ++s){
			for( ii = 1; ii <= d ; ii++)
				if((s >> (ii - 1) & 1)) break ; 
			re aaa = s ^ (1 << (ii - 1)) ; 
			l[s] = l[aaa] * k[ii] / gcd(l[aaa] , k[ii]) ; 
		}
		if(type == 1){
			for(re i = 1 ; i < (1 << d) ; ++i){
				temp = l[i]  ; sum = size[i] ; 
				if(sum % 2) ans += n / temp ; 
				else ans -= n / temp ; 
			}
		} 
		else {
			for(re i = 1 ; i < (1 << d) ; ++i){
				sum = size[i] ; temp = l[i] ; 
				if((sum - 1 )% 2)
				ans -= (ksm(2 , sum -1 ) * (n / temp) ); 
				else ans += (ksm(2 , sum -1 ) * (n / temp) );
			}
		} 
		printf("%lld\n" , ans) ; 
	}
}

DAY4
换教练了。。。。。。
难度突增。。。。。

问题描述
小 K 出了 m 道题,并且找了 n 个选手来给题目打分。每个选手可以给每道
题评分为”Y” 或”N”,同时小 K 认为一道题是优秀的当且仅当有人给这道题评分
为”Y”,同时有人给这道题评分为”N”。
现在所有选手都完成了评分,但是小 K 不认为每个选手的评分都应该采用。于
是小 K 决定对于每一个选手的评分,都有 0.5 的概率采用,0.5 的概率不采用。(注
意可能会导致无人参与评分)
现在,小 K 想知道,对于任意一种题目的集合(例如有两道题,则有三个集合
{1}{2}{1,2}),集合中所有题都为优秀的概率是多少?
为了方便计算,对于总共 2m 1 个集合,假设第 i 个中题目全为优秀的概率
为 Pi,请输出
(P1^2n mod (109+7))xor(P2^2n mod (109+7))xor . . . xor(P(2m-1)^2n mod (109+7))
样例输入
2 
2 2 
NY YN
4 2
NN NY YN YY
输出
1
7

看到 n 很大,m 却很小的时候就该往状压和容斥上面想了。实际上这
道题正是一个基础的容斥。怎么又是容斥
首先我们可以发现,直接计算一个集合中的题目是否全为优秀不好计
算,但是计算集合中题目全不优秀很好计算。(正难则反的思想)
假设题目集合为全集,此时我们不能同时选择任意两个评分不同的选
手。因此假设每种评分有 x 个选手,方案数则加上 2^x - 1。不要忘记最后
加上空集的一种方案。
具体的,我们把每一个选手的评分看成一个二进制数,使用一个数组
统计每一个数的出现次数;为我们计算 Si 的时候,枚举 Si 的所有子集 s,
计算 s 中题目评分为’Y’,Si - s 中评分为’N’,其余题目评分随意的方案数。
如此我们得到了对于任意一个题目集合全不优秀的方案数,但我们要
求的是全部优秀的方案数。
实际上我们可以通过全不优秀的情况求出不全优秀的情况,再用全集
一减即可得到答案。这就是一个经典容斥了

下面给出巧妙的代码实现

#include<bits/stdc++.h>
using namespace std ;
#define maxn 1000010 
#define int long long
#define kkk signed main
inline int read(){
	int ans = 0 , f = 1 ; char ch = getchar() ;
	while ( ch < '0' || ch > '9') {	 if( ch == '-' )  f = -1 ; ch = getchar() ; }
	while ( ch >= '0' && ch <= '9' ) ans =  ( ans << 3 ) +  ( ans << 1 ) + ch - '0' , ch = getchar() ;
	return ans * f ;
}
#define mod 1000000007
int cnt[maxn] ; 
int a[maxn] ; // a : 
int mi[maxn] , bc[maxn] ; 
int T , n , m , ans , temp; 
char in[maxn] , s[maxn]; 
bool vis[maxn] ;
int lowbit(int x){
	return x & (-x) ; 
}
inline void dfs(int x){
	vis[x] = 1 ; 
	a[x] = mi[cnt[0]] ; 
	for(int u = x ; u ; u = (u - 1) & x)
		a[x] += mi[cnt[u]] - 1 , a[x] %= mod ;
	for(int i = 1 ; i <= m ; i++)
		if(((1 << (i - 1)) & x) && !vis[x ^ (1 << (i - 1))]){
//			printf("x : %lld\n" , x) ; 
			for(int u = x ; u ; u = (u - 1) & x){
				if(u & (1 << (i - 1))) cnt[u ^ (1 << (i - 1))] += cnt[u] ; 
			}
			dfs(x ^ (1 << (i - 1))) ; 
			for(int u = x ; u ; u = (u - 1) & x){
				if(u & (1 << (i - 1))) cnt[u ^ (1 << (i - 1))] -= cnt[u] ; 
			}
		}
}
#define File(s) freopen(s".in","r",stdin),freopen(s".out","w",stdout)
kkk(){
	File("survey");
	T = read() ; 
//	printf("T : %lld\n" , T) ; 
	mi[0] = 1 ;
	bc[0] = 0 ; 
	for(int i = 1 ; i < maxn ;i++) mi[i] = (mi[i - 1] << 1) % mod ; 
	for(int i = 1 ; i < (1 << 16) ;i++) bc[i] = bc[i ^ lowbit(i)] + 1 ; 
	while(T--){
//		printf("T : %lld\n" , T) ; 
		n = read() , m = read() ; 
		for(int i = 1 ; i <= n ; i++){
			scanf("%s" , in + 1) ; 
			temp = 0 ; 
			for(int j = 1 ; j <= m ; j++)
			temp |= (in[j] == 'Y' ? 1 : 0) << (j - 1) ; 
			cnt[temp]++ ; 
		}
		vis[0] = 1 ; ans = 0 ; 
		dfs((1 << m) - 1) ; 
		for(int i = 1 ; i < (1 << m) ; i++){
			int now = 0 ; 
			for(int u = i ; u ; u = (u - 1) & i)
				now += bc[u] & 1 ? a[u] : -a[u] , now %= mod ; 
			now = (mi[n] - now) % mod ; 
			now = (now + mod) % mod  ;
			ans ^= now ; 
		}
//		cout << "YeS" << endl ; 
		printf("%lld\n" , ans);
		for(int i = 0 ; i < (1 << m ) ; i++)
		cnt[i] = a[i] = vis[i] = 0;
	} 
}

注意这里枚举子集的子集的复杂度是n^3的
至于为什么呢 emmmmmm 蒟蒻不知道诶。。。

DAY5几乎每道题都难。。。 等我写完正解再补吧。。。
感觉就是noipday2的难度了我还是太弱了

如果对于解析有不懂请留言代码有点丑因为都是我写的

虽然只是我个人模拟赛的解析 但我觉得这些思路是可以借鉴的 代码实现也很巧妙

upd2020.8.16 0:10

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值