You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

7.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

HDU-4734-F(x)

题目传送门

一、题目描述

题目给了个F(x)的定义:\large F(x) = A_n * 2^{n-1} + A_{n-1} * 2^{n-2} + ... + A_2 * 2^1 + A_1 * 2^0$$A_i是十进制数位,然后给出a、b,求区间[0,b]内满足f(i)<=f(a)i的个数。其中 0 <= A,B < 10^9

二、计算题目中的F函数

#include <bits/stdc++.h>

using namespace std;
/**
12345-->f(12345)=1*2^4+2*2^3+3*2^2+4*2^1+5*2^0
         f(1234) =1*2^3+2*2^2+3*2^1+4*2^0
       2*f(1234) =1*2^4+2*2^3+3*2^2+4*2^1
所以形成递归表示式
*/
//利用递归计算出F(x)
int f(int x) {
    if (x == 0) return 0;
    return f(x / 10) * 2 + x % 10;
}

//利用循环进行计算
int f2(int x) {
    int res = 0;
    int i = 0;
    while (x) {
        res += x % 10 * (1 << i);
        i++;
        x /= 10;
    }
    return res;
}

int main() {
    cout << f(12345) << endl;
    cout << f2(12345) << endl;
    //极大值是1e9-1=99999999
    cout << f2(1e9 - 1) << endl;
    //输出4599
    return 0;
}

三、暴力怎么做?

用例解析: 以 5 100 为例:即A=5,B=100 F(A)=5*1=5 也就是让我们求一下,在F(0)~F(100)之间,有多少个F(x)<=5

撸起袖子就是干!

#include <bits/stdc++.h>

using namespace std;
int F(int x) {
    if (x == 0) return 0;
    return F(x / 10) * 2 + x % 10;
}
int fa;
int main() {
    int T;
    int num = 1;
    cin >> T;

    while (T--) {
        int x, y;
        cin >> x >> y;
        fa = F(x);
        int cnt = 0;
        for (int i = 0; i <= y; i++)
            if (F(i) <= fa) cnt++;

        printf("Case #%d: %d\n", num++, cnt);
    }
    return 0;
}

毫无疑问,如此暴力,只能是TLE,看了一下时间限制,要求500ms,应该是一个数位DP问题。

四、正常的数位DP思路

1、思考暴力dfs怎么做:

int dfs(int pos,int sum,bool limit)

  • pos:我在哪里

  • sum:我这个分身,走到现在,按F(x)的计算办法,已经拼接出的加权前缀和是多少,之所以要携带这个,是因为走完全程时需要判断总的sum是不是小于F(A)

  • limit:是不是贴上界,之所以需要知道这个,是因为每位都需要上一位告诉自己,自己及以后各位是不是能取满,还是只能取一部分

返回值int类型的dfs,思路就是 向后思考 :如果我站在pos位置,并且前面的人告诉了我:以前有多少sum前缀和,是不是贴上界, 然后,我的任务就是计算,在给定的条件下,后续的枚举所有可能数字中,有多少符合条件的数字。

根据这个返回值定义,所以整体的结果就是:dfs(al,0,true)

2、记忆化

根据我们的经验,500ms直接暴力dfs应该也不可能过的了,必须考虑是不是存在重复计算,能不能记忆化。简单想一想,肯定是可以记忆化的:到达哪种状态后,我不管你前面怎么到达我这里,一旦我的状态确定,那么我的后续所有可能中,符合条件的数字个数是一定的,我计算一次,想办法记下来,下回就可以重复利用。

那么按什么来记忆呢?根据经验,limit一般不记,只记忆!limit的状态结果,所以dp[pos][sum]的状态记忆数组定义就出来了。

代码也就很简单了:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>

using namespace std;
const int N = 32;
const int M = 1024 * 10 + 10;
int f[N][M];
int a[N];
int fa;
int F(int x) {
    if (x == 0) return 0;
    return F(x / 10) * 2 + x % 10;
}

int dfs(int pos, int sum, bool limit) {
    if (pos == 0) return sum <= fa;
    if (sum > fa) return 0;
    if (!limit && ~f[pos][sum]) return f[pos][sum];
    int ans = 0;
    int up = limit ? a[pos] : 9;
    for (int i = 0; i <= up; i++)
        ans += dfs(pos - 1, sum + i * (1 << (pos - 1)), limit && i == a[pos]);

    if (!limit) f[pos][sum] = ans;
    return ans;
}

inline int calc(int x) {
    memset(f, -1, sizeof f);
    int al = 0;
    while (x) a[++al] = x % 10, x /= 10;
    return dfs(al, 0, true);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    int T;
    int cnt = 1;
    cin >> T;

    while (T--) {
        int x, y;
        cin >> x >> y;
        fa = F(x);
        printf("Case #%d: %d\n", cnt++, calc(y));
    }
    return 0;
}

但很不幸运,Time Limit Exceeded

五、释放减法的力量

大神原文

某大神写到:刚开始一眼看到这题,算了一下 F(999999999) = 4599,也不是很大,感觉挺简单的,想着dp[pos][sum]不就完事了:sum记录从len(最高位)到pos位上累计的符合条件数字个数;

写出来交了一发,TLE,感觉就有点奇怪,翻了翻网上的题解,说是不能是dp[pos][sum],要是dp[pos][F(A) - sum]才行;

也就是说,新定义dp[pos][comp]comp代表剩下的从第pos位到第1位,累加起来的权重,不能超过comp

这是为什么呢?原因其实很简单:

我们定义dp[pos][sum]的话,显然我们这 T组数据……

每组数据只要B有变化,由于sum是从B最高位(al)往下累加权重,它跟B有密切关系(B的长度al即为最高位),那么dp数组就必须要重新memset(dp,-1,sizeof(dp)),这样才不会出错;

那么我们如果是dp[pos][comp]呢,comp代表从第1位到第pos位最多还能累加起多少权重,那么它就和B没有关系,我们就不需要在输入每组数据后都重新将dp数组全部重置为-1

#include <cstdio>
#include <cstring>
#include <iostream>
#include <cmath>

using namespace std;
const int N = 32;       //数位的长度上限
const int M = 1024 * 10 + 10; //剩余的数大小(状态集合按这个划分)
int f[N][M];            //结果DP数组
int a[N];               //数位拆分出来的数组
int fa;
//利用递归计算出
int F(int x) {
    if (x == 0) return 0;
    return F(x / 10) * 2 + x % 10;
}

/**
 * @param pos   数位位置
 * @param st    剩余对比值,初始值是F(x)
 * @param limit 是不是贴上界
 * @return
 */
int dfs(int pos, int st, bool limit) {
    if (pos == 0) return st >= 0; //如果到达最后并且有剩余表示f(i)<= f(x),计数++
    if (st < 0) return 0;         //中途或最后一旦发生小于0情况剪枝
    if (!limit && ~f[pos][st]) return f[pos][st];
    int ans = 0;
    int up = limit ? a[pos] : 9;
    for (int i = 0; i <= up; i++)
        //st = st - i * 2^ ( p-1 )
        ans += dfs(pos - 1, st - i * (1 << (pos - 1)), limit && i == a[pos]);

    if (!limit) f[pos][st] = ans;
    return ans;
}

inline int calc(int x) {
    int al = 0;
    while (x) a[++al] = x % 10, x /= 10;
    return dfs(al, fa, true);
}

int main() {
    //不加78MS加了31MS效果还是很明显的
    ios::sync_with_stdio(false);
    cin.tie(0);

    int T;
    int cnt = 1;
    cin >> T; //这个数值太BT了最大10000次

    //优化的写法
    memset(f, -1, sizeof f);
    while (T--) {
        int x, y;
        cin >> x >> y;
        fa = F(x); //计算出F(x)值,这是一个固定值
        printf("Case #%d: %d\n", cnt++, calc(y));
    }
    return 0;
}