HDU 5587 Array 递归 规律 数位dp

1 篇文章 0 订阅
1 篇文章 0 订阅

Array

 
 Accepts: 112
 
 Submissions: 324
 Time Limit: 2000/1000 MS (Java/Others)
 
 Memory Limit: 131072/131072 K (Java/Others)
问题描述
Vicky是个热爱数学的魔法师,拥有复制创造的能力。
一开始他拥有一个数列{1}。每过一天,他将他当天的数列复制一遍,放在数列尾,并在两个数列间用0隔开。Vicky想做些改变,于是他将当天新产生的所有数字(包括0)全加1。Vicky现在想考考你,经过100天后,这个数列的前M项和是多少?。
输入描述
输入有多组数据。
第一行包含一个整数T,表示数据组数。T. \left( 1 \leq T \leq 2 * {10}^{3} \right)(1T2103)
每组数据第一行包含一个整数M. \left( 1\leq M \leq {10}^{16} \right)(1M1016)
输出描述
对于每组数据输出一行答案.
输入样例
3
1
3
5
输出样例
1
4
7
Hint
第一项永远为数字11,因此样例1输出11
第二天先复制一次,用0隔开,得到{1,0,1},再把产生的数字加1,得到{1,1,2},因此样例2输出前3项和1+1+2=41+1+2=4.
第三天先得到{1,1,2,0,1,1,2},然后得到{1,1,2,1,2,2,3},因此样例3输出前5项和1+1+2+1+2=71+1+2+1+2=7

思路:虽然题目有写100天后其实那是吓唬人的,因为输入范围只有1e16根本到不到100天的。

此题有多种解法。


方法一:根据Hint找找规律很容易发现:

(1)每一天的长度是上一天长度的二倍加一。

(2)每一天的总和是上一天总和的二倍加上上一天的长度再加上中间那个一。

可以预处理出每一天的长度和总和,然后可以二分找出要求的那项前面最近的一天。

答案就是那天的总和加上中间的一个一再加上后面的一小块。

后面那块可以根据数列的开头递归算出。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll len[65],sum[65];
void init()
{
    len[0]=sum[0]=1LL;
    for(int i=1;i<=60;i++)
    {
        len[i]=len[i-1]*2+1;
        sum[i]=sum[i-1]*2+1+len[i-1];
    }
}
ll solve(ll k)
{
    if(k<=1)return k;
    int l=lower_bound(len,len+61,k)-len-1;
    return sum[l]+1+solve(k-len[l]-1)+(k-len[l]-1);
}
int main()
{
    init();
    int t;
    scanf("%d",&t);
    while(t--)
    {
        ll n;
        scanf("%I64d",&n);
        printf("%I64d\n",solve(n));
    }
    return 0;
}

方法二:其实如果再看仔细点就会发现其实数列的每一项都是那个数二进制1的个数。

第n项n的二进制第n项的值
000000
100011
200101
300112
401001
501012
601102
701113
810001
910012
1010102
1110113
1211002
1311013
1411103
1511114

此题就变成输入m输出从0到m所有数二进制中一的个数和。

然后根据从0开始每一个二进制的位置的01出现是有规律的。

找出每个位置的周期直接计算每个位置二进制一的个数,相加就是答案。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll solve(ll k)
{
    ll res=0;
    for(int i=0;i<=60;i++)
    {
        if((1LL<<i)>k)break;
        ll t=1LL<<(i+1);
        res+=(k+1)/t*t/2;
        if((k+1)%t>t/2)res+=(k+1)%t-t/2;
    }
    return res;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        ll n;
        scanf("%I64d",&n);
        printf("%I64d\n",solve(n));
    }
    return 0;
}

方法三:同样根据方法二的规律。

很明显可以把输入的m变成二进制后数位dp直接求。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
int bit[65];
ll dp[65][65];
ll dfs(int len,int num,int ma)
{
    if(len==0)return (ll)num;
    if(!ma&&dp[len][num]>=0)return dp[len][num];
    int maxx=1;
    if(ma)maxx=bit[len];
    ll sum=0;
    for(int i=0;i<=maxx;i++)
    {
        sum+=dfs(len-1,num+(i==1),ma&&i==bit[len]);
    }
    if(!ma)dp[len][num]=sum;
    return sum;
}
ll solve(ll k)
{
    int len=0;
    while(k)
    {
        bit[++len]=k%2;
        k/=2;
    }
    return dfs(len,0,1);
}
int main()
{
    memset(dp,-1,sizeof dp);
    int t;
    scanf("%d",&t);
    while(t--)
    {
        ll n;
        scanf("%I64d",&n);
        printf("%I64d\n",solve(n));
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值