致出题人:此篇blog中所提到的题目如果侵犯了您的版权,请与me联系,me将及时删除。
说在前面
今天考了一套很奇怪的题,除了第三题还能分类成DP之外,T1和T2的主要思想都是依靠性质的。据教练说是一套老题,写起来感觉确实有点不一样。
考式结果对自己来说不是很理想啦。第一题是一个很简单的性质题,然而me没发现…于是写了个暴力拿了60分。第二题是一个统计区间数量的问题,得出答案只需要O(n),然而预处理需要
O(NN−−√)
。有一个小坑点没有注意到,数据针对这个坑点居然还出了三组数据(对此,我的态度当然是QAQ的)…然后掉了30分。最后一道题是个DP。开始看最后一题的时候还剩一个多小时,想出来DP方程的那一瞬间觉得me好厉害,然后就敲敲敲敲敲….然后MLE了…然后把long long改成int再测也只有10分….然后改着改着发现自己的方法是不对的….最后只好向std屈服。不过std的处理方法的确很巧妙,学学学!
考试的感觉呢,不如昨天的好,感觉想题没有昨天那么投入,开小差都好几次…但是思路更清晰,分析题目的时候歪路走的更少了,没有东想西想什么的。今天还特意多留了一些时间用于把握题目,除了能看出第三题可能要DP,第二题可能会有点难之外,几乎什么都没有把握到=w=(貌似看不出方向的题大多都和题目本身性质有关呢)。
做题方面的话,因为最近还是考到了不少性质题(就是那种只要发现了正确方法,几行就可以搞定的那种题),感觉能摸到一点点套路。偏数学类的题大多是需要列出题目中给的所有(对!所有!)式子,然后看看有没有能化归转化的东西(比如看看可不可以写成同余式用exgcd,或者代换掉一些未知数然后写成递归式,之类的)。其他的话主要是看看题目中有没有什么比较特别的地方(比如某个数据范围很小,或者本来可以开的很大的然而却被限制到一定范围内),没有的话就想一想题目要求什么,我知道什么,再结合一些杂七杂八的定理(抽屉原理,生日冲突之类的,考试的时候能想到这些可能还是需要一些灵感),大概也就能写出来了。
然后是对于一些部分 分很少的难题,即使部分分少,但是得到一分是一分,特别是在那种时间已经不多了的情况下(一个小时及以下),还是果断敲暴力(或者数据分治,呀反正只要保证暴力分,怎么都可以)。
大概是这样一些吧,把自己能想到的都写出来了。
下面还是写写题解
T1
解法
30分:根据题意,我们大概已经可以拿到30分了=w=
60分:可以发现N和M都不超过100000,那么无论怎么操作,较小的那个值也一定不会超过100000。然而K有1000000000那么大,其中肯定存在循环节,我们只需要把这个循环节长度求出来,然后把前面一段不完整的循环节暴力处理,中间那些完整段直接用K取模循环节长度,后面那一段不完整的也暴力处理就可以了。复杂度明显可过。(原来数据很水,如果把数组开到2e7就可以过90分)
正解:我们把题目上所描述的操作写成式子(假设N < M)。首先设出
tot=N+M
。一次操作之后有:
我们可以将上面等式的右侧结果代换成a,b然后再来一次操作,可以发现无论操作多少次,变化是相似的。每次操作,N和M都会翻倍然后取模tot,那么操作K次,N和M就变成了 N∗2K%tot 和 M∗2K%tot ,那么最后的答案也就是这两个值较小的那个。
具体实现可以用快速幂。
下面是自带大常数的代码
注释掉的是60分写法
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;
int N , M , K , xs ;
int vis[2000005] , cnt ;
int s_pow( long long x , int b ){
long long rt = 1 ;
while( b ) {
if( b&1 ) rt = rt * x %( N + M ) ;
x = x * x %( N + M ) ; b >>= 1 ;
}
return rt ;
}
int main(){
scanf( "%d%d%d" , &N , &M , &K ) ;
/*
if( N > M ) swap( N , M ) ;
vis[N] = 0 ;
while( 1 ){
M -= N , N += N , ++cnt , --K ;
if( N > M ) swap( N , M ) ;
if( !K ){
printf( "%d" , N ) ;
return 0 ;
}
if( vis[N] ){
xs = cnt - vis[N] ;
break ;
}
vis[N] = cnt;
}
K %= xs ;
for( ; K ; K -- ){
M -= N , N += N ;
if( N > M ) swap( N , M ) ;
}
*/
printf( "%d" , min( s_pow( N , K ) , s_pow( M , K ) ) ) ;
}
T2
解法
首先预处理出所有坏对。
可以发现坏对我们只需要保留那些最短的就可以了,下面me举个栗子
上图中,1号位置和2,3,4号位置构成坏对。可以发现只要一个区间经过了1,2位置,那么这个区间就已经不合法了,因此1和3,4位置构成的坏对是无效的。
预处理用个桶存一下数字是否出现过,判断对于一个数a[i],是否存在一个数a[j]( j > i )并且a[i]%a[j]==K,只需要枚举(a[i]-K)的因数,判断其是否出现就可以了(正确性显然)。
预处理完了直接计算就好了(计算方法很多,可以随便YY一种写)。
下面是自带大常数的代码
考试的时候我脑抽了,写的链表来判断一个数是否出现过。
mdzz!
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;
int N , K , a[100005] , topp ;
long long ans ;
struct Link_list{
int nxt[100005] , head[100005] ;
void Init(){
memset( nxt , 0 , sizeof( nxt ) ) ;
memset( head , 0 , sizeof( head ) ) ;
}
int fr( int x ){
return head[x] ;
}
void Insert( int x , int p ) {
nxt[p] = head[x] ;
head[x] = p ;
}
void Remove_( int x , int p ) {
head[x] = nxt[p] ;
}
}LK , bgK ;
struct Data{
int L , R ;
Data(){} ;
Data( int L_ , int R_ ):L(L_) , R(R_){} ;
bool operator < ( const Data &A ) const {
return R < A.R ;
}
}d[100005] ;
void preWork(){
register int i , j ;
LK.Init() ; bgK.Init() ;
for( i = N ; i >= 1 ; i -- ){
LK.Insert( a[i] , i ) ;
if( a[i] > K ) bgK.Insert( 0 , i ) ;
}
for( i = 1 ; i <= N ; i ++ ) {
int tmp = a[i] - K , mihoyo = 0x3f3f3f3f ;
LK.Remove_( a[i] , i ) ;
if( a[i] > K ) bgK.Remove_( 0 , i ) ;
if( tmp == 0 ){
if( bgK.fr( 0 ) ) mihoyo = min( mihoyo , bgK.fr( 0 ) ) ;
} else {
for( j = 1 ; j * j <= tmp ; j ++ ){
if( tmp % j ) continue ;
if( j > K && LK.fr( j ) ) mihoyo = min( mihoyo , LK.fr(j) ) ;
if( tmp/j > K && LK.fr( tmp/j ) ) mihoyo = min( mihoyo , LK.fr( tmp/j ) ) ;
}
}
if( mihoyo != 0x3f3f3f3f )
d[++topp] = Data( i , mihoyo ) ;
}
}
long long cal( int now , int L , int R ){
return ( R - now + R - L ) * 1LL * ( L - now + 1 ) / 2 ;
}
void solve(){
sort( d + 1 , d + topp + 1 ) ;
int now = 1 ; ans = 0 ;
for( int i = 1 ; i <= topp ; i ++ ){
int L = d[i].L , R = d[i].R ;
if( L < now ) continue ;
ans += cal( now , L , R ) ;
now = L + 1 ;
}
ans += cal( now , N , N + 1 ) ;
printf( "%lld" , ans ) ;
}
int main(){
scanf( "%d%d" , &N , &K ) ;
for( int i = 1 ; i <= N ; i ++ ){
scanf( "%d" , &a[i] ) ;
}
preWork() ;
solve() ;
}
/*
10 7
1 2 3 7 8 19 10 5 12 20
*/
T3
解法
感觉自己讲不太清楚这个题呢…
首先是DP,
dp[i][j]
表示处理了前i行(层),第i行放了j种颜色的方案数。转移:
dp[i][j]=(∑dp[i−1])∗cnt[a[i]][j]
,其中
cnt[a[i]][j]
表示有
a[i]
个位置,放置
j
种颜色的方案。我们发现直接预处理cnt数组是不行的,需要用到组合数,然而由于M太大,而且不保证模数是质数,因此没有办法计算组合数。
这个时候我们换一种想法,预处理
然后我们再来看这些颜色(上面的x和y)可以怎么选,易得选的方案就是
m∗(m−1)∗⋯∗(m−j+1)
。
最后考虑上一层与当前层重复的情况,因为上面的dp方程中我们多加了不合法的情况,因此我们需要减去那些不合法的。这一层和上一层都选了j种颜色,并且这一层的颜色集合要和上一层的冲突,那么这样的颜色选择方案会有 j! 种。剪掉这些多算的部分即可。
下面是自带大常数的代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;
int N , M , mmod , a[1000005] , maxa ;
int P[5005] , fac[5005] , f[5005][5005] ;
long long dp[2][5005] ;
void preWork(){
fac[0] = 1 ;
for( int i = 1 ; i <= maxa ; i ++ )
fac[i] = 1LL * fac[i-1] * i %mmod ;
P[1] = M ;
for( int i = 2 ; i <= maxa && i <= M ; i ++ )
P[i] = 1LL * P[i-1] * ( M - i + 1 ) %mmod ;
f[1][1] = 1 ;
for( int i = 2 ; i <= maxa ; i ++ )
for( int j = 2 ; j <= M ; j ++ )
f[i][j] = ( 1LL * f[i-1][j] * ( j - 1 ) + f[i-1][j-1] ) %mmod ;
}
void solve(){
int now = 1 , pre = 0 ;
long long signow = 0 , sigpre = 0 ;
for( int j = 1 ; j <= a[1] && j <= M ; j ++ ){
dp[now][j] = 1LL * f[ a[1] ][j] * P[j] %mmod ;
signow = ( signow + dp[now][j] ) %mmod ;
}
for( int i = 2 ; i <= N ; i ++ ){
swap( now , pre ) ;
swap( signow , sigpre ) ;
signow = 0 ;
for( int j = 1 ; j <= a[i] && j <= M ; j ++ ){
dp[now][j] = sigpre * f[ a[i] ][j] %mmod * P[j] %mmod ;
if( j <= a[i-1] )
dp[now][j] = ( dp[now][j] - dp[pre][j] * f[ a[i] ][j] %mmod * fac[j] %mmod + mmod ) %mmod ;
signow = ( signow + dp[now][j] ) %mmod ;
}
}
printf( "%lld" , signow ) ;
}
int main(){
scanf( "%d%d%d" , &N , &M ,&mmod ) ;
for( int i = 1 ; i <= N ; i ++ )
scanf( "%d" , &a[i] ) , maxa = max( maxa , a[i] ) ;
preWork() ;
solve() ;
}