2017.11.8机房小测-题目性质/区间计数(杂题)/DP状态巧定义

致出题人:此篇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 。一次操作之后有:

N=N+N=(2N)%totM=MN=M(totM)=2Mtot=2M%tot

我们可以将上面等式的右侧结果代换成a,b然后再来一次操作,可以发现无论操作多少次,变化是相似的。每次操作,N和M都会翻倍然后取模tot,那么操作K次,N和M就变成了 N2K%tot M2K%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[i1])cnt[a[i]][j] ,其中 cnt[a[i]][j] 表示有 a[i] 个位置,放置 j 种颜色的方案。我们发现直接预处理cnt数组是不行的,需要用到组合数,然而由于M太大,而且不保证模数是质数,因此没有办法计算组合数。

这个时候我们换一种想法,预处理f[i][j]表示有 i 个位置,放置j个不同的颜色的方案数,注意这个 j 个不同的颜色指的是颜色类型不同(即像<1,2,1>,<2,3,2>,< x,y,x>这样的,我们看作是同一种)。这个可以用递推式解决,初值f[1][1]=1,转移 f[i][j]=f[i1][j](j1)+f[i1][j1]
然后我们再来看这些颜色(上面的x和y)可以怎么选,易得选的方案就是 m(m1)(mj+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() ;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值