模拟30—「毛一琛·毛二琛·毛三琛」
毛一琛
考场上想的折半搜索。
先枚举全部集合。
然后双指针找和他相等的,然后看有没有交集。
如果没有交集,和还跟他相等,那他就能对答案贡献\(1\)
其实复杂度是\(O(2^n*2^{n/2} = 2^{3n/2} )\) 最大 \(1e9\)
这还是假设双指针指针单调的情况下的复杂度,由于有很多相等的,指针不单调,复杂度可能会更高。
正解也是折半搜索。
先 \(3^{n/2}\) 枚举所有集合,和集合补集的子集。
设他们的权值分别是 \(a,b\) ,那么,如果左边右边有 \(a-b=c-d => a+d=c+b\) ,就说明我们找到一种合法方案。
所以我们把他们的权值做差之后的权值塞到map<int,vector<int>> 里面,存集合和集合补集的子集的并,这样我们就可以快速找到 \(a-b\) 和其对应的选择状态。
然后重要的一步操作是去重,这样一个权值最多对应 \(2^{n/2}\) 种状态
\(STL\) 去重方法如下。
//STL知识++ , map 套 vector , map里面相当于是一个pair数组,只是增加了第一维索引第二维的功能
//所以我们调用迭代器 map< int , vector< int > > ::iterator it = mp.begin() ; it != mp.end() ; it ++
//it 返回的是一个 pair 的指针,指针类型可以直接用 -> 进行索引 ,索引到vector 之后他就变成了一个vector
// vector 的unique之后 size 是不变的,所以需要resize ;
// sort( v.begin() , v.end() ) ;
// int newsize = unique( v.begin() , v.end() ) - v.begin() ;
// v.resize( newsize ) ;
然后在\(O(3^{n/2})\)去右边暴力找每一种集合和其补集的子集,算出他们对应的差值,去左边的 \(map\) 里面找(由于去过重,最多 \(2^{n/2}\)),进行合并,匹配,最后用一个 \(2^n\) 的布尔数组记录一下哪个值会造成贡献就行了。
code
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <bitset>
#include <iostream>
#include <vector>
#include <map>
#define R register int
#define scanf Ruusupuu = scanf
#define freopen rsp_5u = freopen
int Ruusupuu ;
FILE * rsp_5u ;
using namespace std ;
typedef long long L ;
typedef double D ;
const int N = 1e6 + 10 ;
const int M = 1e9 + 10 ;
inline void of(){ freopen( "in.in" , "r" , stdin ) , freopen( "out.out" , "w" , stdout ) ; }
inline void cf(){ fclose( stdin ) , fclose( stdout ) ; }
inline int read(){
int w = 0 ; bool fg = 0 ; char ch = getchar() ;
while( ch < '0' || ch > '9' ) fg |= ( ch == '-' ) , ch = getchar() ;
while( ch >= '0' && ch <= '9' ) w = ( w << 1 ) + ( w << 3 ) + ( ch ^ '0' ) , ch = getchar() ;
return fg ? -w : w ;
}
int n , m [30] , k , h , top , rop , ans , lg [1 << 21] ;
map< int , vector< int > > mp ;
//STL知识++ , map 套 vector , map里面相当于是一个pair数组,只是增加了第一维索引第二维的功能
//所以我们调用迭代器 map< int , vector< int > > ::iterator it = mp.begin() ; it != mp.end() ; it ++
//it 返回的是一个 pair 的指针,指针类型可以直接用 -> 进行索引 ,索引到vector 之后他就变成了一个vector
// vector 的unique之后 size 是不变的,所以需要resize ;
// sort( v.begin() , v.end() ) ;
// int newsize = unique( v.begin() , v.end() ) - v.begin() ;
// v.resize( newsize ) ;
bool fg [1 << 21] ;
inline void debug( int x ){ cout << bitset<10>(x) << endl ; }
void sc(){
n = read() ;
for( R i = 1 ; i <= n ; i ++ ) m [i] = read() ;
}
inline L makp( int x , int y ){
return ( 10000000ll * x + 1ll * y ) ;
}
inline int lb( int x ){ return x & (-x) ; }
inline void calc( int x , int y ){
int org = x | y , ansx = 0 , ansy = 0 ;
while( x ){ ansx += m [lg [lb(x)] + 1] , x -= lb( x ) ; }
while( y ){ ansy += m [lg [lb(y)] + 1] , y -= lb( y ) ; }
mp [ansx - ansy].push_back( org ) ;
}
inline void toans( int x , int y ){
int org = x | y , ansx = 0 , ansy = 0 ;
while( x ){ ansx += m [lg [lb(x)] + 1 + k] , x -= lb( x ) ; }
while( y ){ ansy += m [lg [lb(y)] + 1 + k] , y -= lb( y ) ; }
int r = mp [ansx - ansy].size() ;
for( R i = 0 ; i < r ; i ++ )
fg [mp [ansx - ansy][i] | ( org << k )] = 1 ;
}
void work(){
lg [0] = -1 ;
for( R i = 1 ; i <= ( 1 << n ) ; i ++ ) lg [i] = lg [i >> 1] + 1 ;
k = n >> 1 , h = n - k ;
for( R i = 0 ; i < ( 1 << k ) ; i ++ ){
int u = i ^ ( ( 1 << k ) - 1 ) ;
for( R j = 0 ; j < ( 1 << k ) ; j ++ )
if( ( u & j ) == j ) calc( i , j ) ;
}
for( map< int , vector< int > >::iterator it = mp.begin() ; it != mp.end() ; it ++ ){
sort( (it->second).begin() , (it->second).end() ) ;
int newsize = unique( (it->second).begin() , (it->second).end() ) - (it->second).begin() ;
(it->second).resize( newsize ) ;
}
for( R i = 0 ; i < ( 1 << h ) ; i ++ ){
int u = i ^ ( ( 1 << h ) - 1 ) ;
for( R j = 0 ; j < ( 1 << h ) ; j ++ )
if( ( u & j ) == j ) toans( i , j ) ;
}
for( R i = 1 ; i < ( 1 << n ) ; i ++ ) ans += fg [i] ;
printf( "%d\n" , ans ) ;
}
signed main(){
// of() ;
sc() ;
work() ;
// cf() ;
return 0 ;
}
毛二琛
考场上看错题了,浪费了一道好题。
其实这个东西需要先转化一下题意。
可以发现,交换可能有很多种方案,但是他们中间总有一些约束。
我们设置 \(reletion_i\) 表示 第 \(i\) 个和第 \(i + 1\)个需要哪个先操作,根据各个数的位置,可以求出来这个数组,当然,如果被重复覆盖,那么直接执行 die。
具体方法就是 如果 第 \(i\)个数在第 \(p_i\)的位置上,那么\(p_i\) 就需要先被换,中间的一大部分同理,同时还要保证 \(i\) 已经被换过了,因为i换玩之后我们就把他放到了正确的位置上。
有了这个关系之后其实就是求满足这个关系的序列有多少种,如果我们设置 \(f_{i,j}\) 表示第 \(i\) 个位置放 \(j\) 的方案数,会发现转移的时候只能知道 第 \(i-1\) 个数是啥,无法转移,所以需要设置 \(f_{i,j}\) 表示 前 \(i\) 个数,第 \(i\) 个在前 \(i\) 个里面排 \(j\) 的方案数。
转移很简单。
显然可以前缀和优化,然后就 \(n^2\) 了
code
#include <cstdio>
#include <cstring>
#include <algorithm>
#define int long long
#define R register int
#define scanf Ruusupuu = scanf
#define freopen rsp_5u = freopen
int Ruusupuu ;
FILE * rsp_5u ;
using namespace std ;
typedef long long L ;
typedef double D ;
const int N = 5e3 + 10 ;
const int P = 1e9 + 7 ;
inline void of(){ freopen( "in.in" , "r" , stdin ) , freopen( "out.out" , "w" , stdout ) ; }
inline void cf(){ fclose( stdin ) , fclose( stdout ) ; }
inline int read(){
int w = 0 ; bool fg = 0 ; char ch = getchar() ;
while( ch < '0' || ch > '9' ) fg |= ( ch == '-' ) , ch = getchar() ;
while( ch >= '0' && ch <= '9' ) w = ( w << 1 ) + ( w << 3 ) + ( ch ^ '0' ) , ch = getchar() ;
return fg ? -w : w ;
}
int n , p [N] , rel [N] , f [N][N] , sum [N][N] ;
inline void A( int &a , int b ){ a = ( a + b ) >= P ? ( a + b - P ) : ( a + b ) ; }
inline int AA( int a , int b ){ return ( a + b ) >= P ? ( a + b - P ) : ( a + b ) ; }
inline int JJ( int &a , int b ){ return ( a - b ) < 0 ? ( a - b + P ) : ( a - b ) ; }
inline void die(){ puts( "0" ) , exit( 0 ) ; }
void sc(){
n = read() ;
for( R i = 1 ; i <= n ; i ++ ) p [i] = read() + 1 ;
}
inline void make( int pos , int val ){
if( !pos || pos == n - 1 ) return ;
if( rel [pos] == 3 - val ) die() ;
rel [pos] = val ;
}
inline void set( int l , int r , int cs ){
make( l - 1 , cs ) , make( r - 1 , cs ) ;
for( R i = l ; i < r - 1 ; i ++ ) make( i , 3 - cs ) ;
}
void work(){
for( R i = 1 ; i <= n ; i ++ )
if( p [i] > i ) set( i , p [i] , 2 ) ;
else if( p [i] < i ) set( p [i] , i , 1 ) ;
for( R i = 1 ; i < n ; i ++ ) sum [1][i] = 1 ;
for( R i = 2 ; i < n ; i ++ ) for( R j = 1 ; j <= i ; j ++ ){
if( rel [i - 1] == 1 ) f [i][j] = sum [i - 1][j - 1] , sum [i][j] = AA( sum [i][j - 1] , f [i][j] ) ;
else f [i][j] = JJ( sum [i - 1][i - 1] , sum [i - 1][j - 1] ) , sum [i][j] = AA( sum [i][j - 1] , f [i][j] ) ;
}
/* f [1][1] = 1 ;
for( R i = 2 ; i < n ; i ++ ) for( R j = 1 ; j <= i ; j ++ ){
if( rel [i - 1] == 1 ) for( R k = 1 ; k < j ; k ++ ) A( f [i][j] , f [i - 1][k] ) ;
else for( R k = j ; k < n ; k ++ ) A( f [i][j] , f [i - 1][k] ) ;
printf( "%lld %lld %lld\n" , i , j , f [i][j] ) ;
}*/
int ans = 0 ;
for( R i = 1 ; i <= n - 1 ; i ++ ) A( ans , f [n - 1][i] ) ;
printf( "%lld\n" , ans ) ;
}
signed main(){
// of() ;
sc() ;
work() ;
// cf() ;
return 0 ;
}
毛三琛
枚举 \(P\) 之后显然是二分答案的板子,但是问题就在于这个枚举 \(P\) ,这里只用加入一个最优化剪枝。
由于具有单调性,如果之前的最优解用新的数组 \(check\) 一下。
返回值是 \(0\) , 那么这个新数组一定不会更新最优解。
这样,复杂度就大大减小,考虑到出题人可能卡,所以我们random_shuffle一下,这样由于随机数的性质,就没法卡了。
code
#include <cstdio>
#include <cstring>
#include <algorithm>
#define R register int
#define scanf Ruusupuu = scanf
#define freopen rsp_5u = freopen
int Ruusupuu ;
FILE * rsp_5u ;
using namespace std ;
typedef long long L ;
typedef double D ;
const int N = 1e4 + 10 ;
inline void of(){ freopen( "in.in" , "r" , stdin ) , freopen( "out.out" , "w" , stdout ) ; }
inline void cf(){ fclose( stdin ) , fclose( stdout ) ; }
inline int read(){
int w = 0 ; bool fg = 0 ; char ch = getchar() ;
while( ch < '0' || ch > '9' ) fg |= ( ch == '-' ) , ch = getchar() ;
while( ch >= '0' && ch <= '9' ) w = ( w << 1 ) + ( w << 3 ) + ( ch ^ '0' ) , ch = getchar() ;
return fg ? -w : w ;
}
int n , p , k , a [N] , b [N] , res = 0x3f3f3f3f , c [N] ;
void sc(){
n = read() , p = read() , k = read() ;
for( R i = 1 ; i <= n ; i ++ ) a [i] = read() ;
}
inline bool check( int x ){
int now = 0 , part = 1 ;
for( R i = 1 ; i <= n ; i ++ ){
if( b [i] + now <= x ) now += b [i] ;
else if( b [i] > x ) return 0 ;
else now = b [i] , part ++ ;
if( part > k ) return 0 ;
} return 1 ;
}
void work(){
for( R i = 0 ; i < p ; i ++ ) c [i] = i ;
random_shuffle( c , c + p ) ;
for( R i = 0 ; i < p ; i ++ ){
int sum = 0 ;
for( R j = 1 ; j <= n ; j ++ ) b [j] = ( a [j] + c [i] ) % p , sum += b [j] ;
if( !check( res ) ) continue ;
int lside = 0 , rside = sum , ans = sum ;
while( lside <= rside ){
int mid = ( lside + rside ) >> 1 ;
if( check( mid ) ) ans = mid , rside = mid - 1 ;
else lside = mid + 1 ;
}
res = min( res , ans ) ;
} printf( "%d\n" , res ) ;
}
signed main(){
// of() ;
sc() ;
work() ;
// cf() ;
return 0 ;
}
总结
这场又炸了,原因如下
- T2没打暴力,不过我确实想打暴力,奈何没看懂题,导致思考也很少。
- T1复杂度算假了,没学过双指针就乱搞,并且\(1e9\)的复杂度也不对。主要是认为自己想到正解后心态的波动,浪费了很多时间去对拍调试,最后发现根本复杂度就不对。
- T3少想了许多,打了个暴力就走人了。
一些收获
-
T1我想到按物品的贡献分组然后 \(n^2\) 爆扫了,但是我看没有这种的数据范围就没打,期望是这个东西大概率不会被卡死,要卡他不可能所有点都去卡这个(预言待翻车),所以考试的时候想到一些复杂度没太大保证但是看起来不会很惨的算法的时候可以考虑一打。
-
想到解法之后要认真算时空复杂度,不要因为心态波动而考虑不周全。
-
T2看题,要相信样例解释,随便一个小暴力就 \(20pts\) 了,何况还能增加思考。
-
T3 这种对于想不到正解,可以考虑加上一些小剪枝,我总是想着反正是暴力就这么多分就破罐子破摔了,但是如果好好剪枝就能多很多分甚至 \(AC\) 掉,所以之后打暴力,对着前两档想的时候,想想还能剪啥枝,多注意一下细节,养成好习惯,到时候就会自然而然的注意细节,小剪枝也加满了,赘余的能不算就不算。
-
打暴力的时候心不能着急想着还要打正解,心态真的太关键了,每场心态波动导致少拿很多分,没有强大的实力就不要选择自己驾驭不了的考试策略。

浙公网安备 33010602011771号