YBT进阶一年游P1 递归与递推

YBT进阶一年游P1 递归与递推

这一系列的题都来自于ybt高效进阶,相当有难度,所以以后尽可能坚持每一部分的题都写一篇博客进行总结。本人水平很一般,能力很有限,故解法和写法不一定(应该说是一定不)最优,希望dalao多加指教。

T1 错排问题


按照一般的递推套路,设a[i]表示i个数错排的方法数,对于这i个数中的任意数j,都有(i-1)个位置可以选择,j选择之后剩余的(i-1)个数再进行错排,故有a[i]=(n-1)a[i-1].
然而这个递推式是错误的。事实上,如果我们把j放在了k位置,看作去掉了一个k位置的话,那么余下的(i-1)个数都可以放在j位置,而k可以放在任意位置,不遵循(总数-1)选择数的约束。通俗一点来讲,去掉了j和k位置以后的情况不构成错排,排法的数目也就不能用a[i-1]代替,而如果我们用a[i-1]代替,会错过一些情况。
那么我们不妨考虑一下到底错过了哪些情况。为了研究错过的情况,我们从被拿走了位置的数来研究。
举例来说,如果现在对4个数1 2 3 4进行错排,把1放在4的位置,就剩下了三个数2 3 4和位置1 2 3.在a[i-1]的规则中,2 3 4这三个数和1 2 3完全一样(只考虑数的次序),2不能放在1,3不能放在2,4不能放在3;事实上(只考虑数的大小),2不能放在2,3不能放在3,4可以任意放。我们只是关心到底有多少方案,因此2不能放在1和2不能放在2完全是一回事,反正就是有一个位置不能放而已,3同理。结合事实的角度,a[i-1]忽略的是4可以放在1的情况。当4放在1的时候,剩下的就是2 3两个数和2 3两个位置,此时形成了一个真正的错排a[i-2].
这样一来,a[i]=(n-1)(a[i-1]+a[i-2]),此题就结束了。

T2 数的划分

在这里插入图片描述
这道题和错排思路很类似,如果从n中去掉一个1,余下(n-1)就要再进行(k-1)次划分,所以a[i][j]肯定有一部分是a[i-1][j-1].
另外,n也有不会分成(k-1)个1的情况,这时候我们还是要往分出(k-1)个1的方向考虑。如果把(n-k)按上面的分法分成k份,每一份都加上1,那么就完成了这个任务。因此a[i][j]=a[i-1][j-1]+a[i-j][j].

以上的部分主要难度还是在于普通的递推式推导,而下面的两道题推导过程更综合,难度也大幅上升。

T3 无限序列

在这里插入图片描述
这道题乍一看虽然有点儿复杂,但是推导一下序列的规律依次计数好像就可以了;

然而这个序列长度一出来,O(n)的方法都肯定行不通了,也就是说我们肯定没法求一个a[2^63]求解这题。
冷静一下,先看看序列有什么规律再作打算:
1变成10,长度+1,1的个数不变;0变成1,长度不变,1的个数+1.因此如果用len[i]记录长度、cnt[i]记录1的个数,可以得到
len[i]=len[i-1]+cnt[i-1],
cnt[i]=cnt[i-1]+cnt[len[i-1]-cnt[i-1]].
观察一下这个朴素的式子背后的含义。由于一切都是从一个1开始,我们从10开始分析,10会变为101,由于1变为10,故前两位不变;101当中1又能变成10,0变成1,得到的10110前三位还是101…以此类推,每一次得到的新序列只是在原基础上加了一段。
考虑加的部分,10中,1维护10,0产生了新的1;101中,10维护101,1产生了新的10,下一次就应该是101维护10110,而10又产生101…以此类推,每次多出来的部分会产生新的部分,而新的部分也会一边维护一边又产生新的部分,不断循环下去。因此最后的递推式应该是:
len[i]=len[i-1]+len[i-2]
cnt[i]=cnt[i-1]+cnt[i-2].
这样一来就产生了一个斐波那契式的规律,而即使长度大如2^63,推导len和cnt的次数也不过不到63次,完全不用担心TLE。
同时,利用len和cnt,根据刚才推导的序列产生原理,我们就能够一步步推出要求的大序列是由哪些小序列拼接而成(毕竟所有的序列都会维护自己),进而求解(很显然,求1–b和1–(a-1)并作差求解最方便)。这里的寻找过程需要用递归实现。
完整的代码如下:

#include<cstdio>
using namespace std;
long long ret,cnt[1000001],len[1000001];
void yjx(long long x,long long j){
    int i;
    if(x == 0) return;
    for(i = j;i >= 1;i--){
        if(len[i] <= x){
            ret += cnt[i];
            yjx(x - len[i],j - 1);
            break;
        }
    }
    return;
}
int main(){
    long long a,b,n,m,i,j,res;
    scanf("%lld",&m);
    for(i = 1;i <= m;i++){
        res = 0,ret = 0;//由于有多组数据,不要忘记初始化
        scanf("%lld %lld",&a,&b);
        cnt[1] = cnt[2] = len[1] = 1,len[2] = 2;
        for(j = 3;len[j - 1] <= b;j++){
            cnt[j] = cnt[j - 1] + cnt[j - 2];
            len[j] = len[j - 1] + len[j - 2];
        }
        yjx(b,j - 1);
        res = ret;
        ret = 0;
        yjx(a - 1,j - 1);
        res -= ret;//写法比较乱......
        printf("%lld\n",res);
    }
    return 0;
}

T4 序列个数

诚实吐槽一句,这题第一个难点就在于能不能看懂题…

简单解释一下就是,从n的全排列b[n]中选出排法,使得对于任意一个数i,小于他的b[i]都有a[i]个(感觉跟没解释一样 )。
这个题同时要考虑到两个要素,一是b[i]的位置,二是b[i]和a[i]的值作比较。因此我们采取看着i和a[i]填b[i]的方式。

说实话,这个方法虽然我基本能想到,但是不如题解的清楚,所以以下内容基本全是题解的功劳(捂脸)。

由于要看两个东西,所以画一个二维的矩阵,一维代表了序数,一维代表了值(显然这个矩阵是个正方形)。
在这里插入图片描述

(横坐标x代表序数,纵坐标y代表值,填入代表x位置填入值y)
这样一来,a[i]就表示表示以(1,1)为左上角,(i,i)为右下角的矩阵中填入的数字个数,因此a[i]-a[i-1]表示一个L形区域内填入的数字个数。很显然,同一行、同一列不能重复填。
讨论一下a[i]-a[i-1]的规律:
如果差为0,说明新增加的部分不需要填数,跳过。
如果差为1,说明新增的部分需要填一个数。第i个L(我们认为(1,1)这一格就代表第一个L)可以填的格有(2i-1)个,受前面限制而不能填的有2c[i-1]个,故res*=(2*i-1-2 *c[i-1]).
如果差为2,说明新增的部分需要填两个数,每一个数可以填的格都有(i-1)个(很显然角上那格没办法填),而受前面限制而不能填的有c[i-1]个,故res=res *(i-1-c[i-1]) *(i-1-c[i-1]).
如果差大于2,根本没办法填,res=0.
这样一来,所有的情况均已讨论,就可以求res。

接下来以样例为例进行模拟尝试,如果读者可以理解,可跳过这一部分。

样例:
输入
3
0 1 3
输出
3
a[1]=0, a[2]-a[1]=1, a[3]-a[2]=2.
对于第一个L,不填,跳过,res=1.
对于第二个L,需要填一个,可行的填法有三种,res=1*3=3.
对于第三个L,需要填两个,每一个数可行的填法有一种,res=3 *1^2=3.
一种填法示意图如下:
在这里插入图片描述

#include <cstdio>
using namespace std;
long long a[10001];
int main() {
    long long n, i, res = 1;
    scanf("%lld", &n);
    for (i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
    }
    for (i = 2; i <= n; i++) {
        if (a[i] - a[i - 1] == 0)
            continue;
        if (a[i] - a[i - 1] == 1)
            res = res * (2 * i - 1 - 2 * a[i - 1]) % 340610;
        if (a[i] - a[i - 1] == 2)
            res = res * ((i - 1 - a[i - 1]) % 340610) * ((i - 1 - a[i - 1]) % 340610) % 340610;
        if (a[i] - a[i - 1] > 2) {
            res = 0;
            break;
        }
    }
    printf("%lld", res);
    return 0;
}

简单地作个个人总结:
递归和递推的解题应该先考虑i状态和i-1状态(甚至是i-2)之间的联系,根据联系的维度确定解题方法,根据关系找递推式。有的时候会利用a[i]-a[i-1]求解,这时候可以不用数组代表结果,而改为用res代表结果(总之要根据题干的要求来选择)。有时候也利用特殊值、初始值帮助我们进行推导。
有的时候一些问题的递推式不容易得出,要确定自己现在a[i]代表的意义,并把其他的情况尽量化为a[i]的情况(例如T1的错排转化)。

以上就是YBT P1的学习总结,如果看到这篇博客的朋友有更多心得体会欢迎分享,也欢迎大家批评指正。
Thank you for reading!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值