【算法学习笔记】43.动态规划 逆向思维 SJTU OJ 1012 增长率问题

Description

有一个数列,它是由自然数组成的,并且严格单调上升。最小的数不小于S,最大的不超过T。现在知道这个数列有一个性质:后一个数相对于前一个数的增长率总是百分比下的整数(如5相对于4的增长率是25%,25为整数;而9对7就不行了)。现在问:这个数列最长可以有多长?满足最长要求的数列有多少个?

Input Format

输入仅有一行,包含S和T两个数( 0<S<T200000 )。

30%的数据,0<S<T100 ;

100%的数据,0<S<T200000

Output Format

输出有2行。第一行包含一个数表示长度,第二行包含一个数表示个数。

Sample Input

2 10

Sample Output

5
2

样例解释

2 4 5 6 9以及2 4 5 8 10

 

看着第一感觉就肯定是要用DP的,所以思考从S到T的问题,如何变为S到T-1的问题呢?

首先我们需要知道S到T-1的最长序列有几个,它们的末端分别是谁,然后把T和他们的末端进行比较,看是否可以连上,从而决定是否增加长度,是否更新最长长度的个数。

因此,我们DP的状态就是 d[i]表示以i为结尾的最长序列的长度 和 times[i]以i结尾的最长序列的个数 还有 cnt[x]长度为x的序列的个数

算法过程:

从S到T遍历每个起点i

接下来有两条路可以走: 正向思考 我们可以遍历从i+1到T的每一个数去和i进行比较,判断是否满足条件,但是这样肯定会超时,因为数据量太大。

另外一个想法就是逆向思维,我们最终的判断是需要找到增长率的百分比为整数的那个数。

假设百分下整数为j,则确定的那个数 tmp 与 i的关系为

(tmp-i) / i = j / 100 整理可以得到 tmp = i + i*j/100 (可以看出 tmp为整数的条件要求 i*j%100==0)

得到了这个tmp(必须小于等于T)之后,开始进行DP状态转移

 

  状态转移关系如下: 

  首先,我们的tmp是在以i为结尾序列之后添加的,长度为d[i]+1, 以tmp的结尾的最长的序列长度和次数可能要更新

  如果d[tmp]和d[i]+1相等, 说明在此之前 以tmp结尾的序列的最长长度 和 刚刚得到的序列长度一致

    所以要将 times[tmp] += times[i] //在这个序列的d[i]部分的重复度为times[i] ( >=1 )

  如果d[tmp]<d[i]+1,说明新的序列长度比原来的还要长,所以不仅要更新最长长度d[tmp] 还要更新 最长长度的重复次数times[tmp]

    d[tmp] = d[i] + 1

    times[tmp] = times[i]

  之后,我们要更新总的最大长度ans :  ans = max(ans,d[tmp])

            更新当前序列所占长度的序列数 cnt[d[i]+1] += tims[i] * 1; (*1是为了更好的说明 times[i]的是d[i]部分的重复度 1表示tmp的重复度 , 和1285里的一个部分很像)

至此状态转移结束

最后只需输出 ans 和 cnt[ans]即可

PS:说明一点,就是为什么j从1到100即可。 增长率从1%到100%的覆盖区间恰好是,i到2i这个部分。

那么为什么是2i不是3i呢。可以举个例子,比如序列x,x,x,x,x,i,x,x,x,3i的长度一定没有x,x,x,x,x,i,x,,2i,x,x,3i 的长度长,所以考虑最优解只要在i到2i里找下一位即可。

当然,更严谨的数学证明会比较麻烦,需要证明在2i之后的每一个能够满足条件的tmp都可以从i,tmp 可以转换至少为 i,x,tmp的结构 其中x小于2i。 有空再证明吧,反正AC了。。

代码如下:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long ll;

const int maxn=200007;
int d[maxn];
ll cnt[maxn],times[maxn];
//cnt[i]存储的是 长度为i的序列的个数
//d[i]存储的是以i结尾的序列最长的长度
int main()
{
    int s,t;
    cin>>s>>t;
    memset(cnt,0,sizeof(cnt));
    int i,j,tmp,ans=1;
    cnt[1]=t-s+1;

    for(i=s;i<=t;i++)
        d[i]=times[i]=1; //初始化为1

    for(i=s;i<=t;i++) //遍历每个数作为起点
        for(j=1;j<=100;j++)    if( (i*j)%100 == 0) //假设增长率为j%
        {
            //则在此增长率下 得到的数为 tmp 可以看出,要让tmp为整数 必须i*j是100的倍数
            tmp = i + i*j/100;
            if( tmp <= t ) //如果这个tmp是在规定范围内的 我们就找到了一个解
            {
                if(d[i]+1 > d[tmp]) //这时要比较 看看要不要更新以tmp结尾的最长的长度
                {
                    d[tmp] = d[i] + 1;//以tmp为结尾的序列的长度 为 以i为结尾的序列的长度 再加上1 表示算上tmp
                    times[tmp] = times[i]; //发生了最长长度的更新 所以要重置 以tmp结尾的最长长度的重复次数为 i的
                }
                else if(d[i]+1==d[tmp]) //恰好是重复的最长长度 
                {
                    times[tmp] += times[i];//则最长长度的次数 增加 以i结尾的最长长度重复次数
                }

                ans = max(ans,d[tmp]);  //更新ans
                //d[i]+1 表示以i结尾的序列长度+tmp所增加的1位 这个长度的子列数量要增加times[i]
                cnt[d[i]+1] += times[i]; 
            }
        } 
    cout<<ans<<cnt[ans];    
    return 0;
}

 

 

 

 

 

转载于:https://www.cnblogs.com/yuchenlin/p/sjtu_oj_1012.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值