33oj 2025练习赛1-F

0、*

前言

这道题简直是太难了,前后干了4~5天!T^T
细节超多啊啊啊!
注意WARNING: 本篇题解写得非常垃圾/乱,且本题非常难/恶心/ 毒瘤 ,请注意脑袋不要爆掉。

温馨提示:请尽量缩小窗口,不然Latex可能会炸掉。

题目传送门

33oj 好数-题目详情

1、题面

题目描述

称一个正整数为好数,当且仅当它的所有数位从左往右不增或不降(特别地,一位数也为好数)。
如 $ 1,125,662,8899 $ 都是好数,但是 $ 132,7723 $ 都不是。现在给你一个好数,询问它是第几大的好数。

输入格式

第一行一个数字 \(T\),表示数据组数。
接下来 \(T\) 行,一行一个好数 \(n\)

输出格式

输出 \(T\) 行,每行一个数字,表示答案。

题目范围

$ 1 \leq T \leq 10^6, 1 \leq n \leq 10^{18}。 $

2、思路过程

我们假设 \(n\)\(sz\) 位。

首先一眼瞟到数据范围,率先否认掉 $ O(Tn) $ 的纯暴力(虽然可以骗到20分,加点优化40分)。

然后很快发现 开longlong

继续观察,显然:求 \(n\) 是第几个好数,相当于求 $ \leq n $ 的好数有多少个。

继续考虑时间复杂度,正解的复杂度很有可能为 \(O(T×sz)\) ,也许还要乘上一个常数 \(10\) ,不能再多了,否则直接TLE,所以初步可以判断位按位枚举。

那么这道题要怎么做呢?

我们分开思考上升和下降

上升(不降):

思考到:我们应该是首先考虑小于 \(sz\) 位的所有数字的上升方案,然后是对于每一位进行枚举,将所有小于该位且大于等于上一位(如果没有上一位则就是1)的数字全部进行计算:当这个数字是倒数第 \(K\) 位时,且大小不限制(因为如果某位小于另一个数的那一位,且前面每一位两数相等,那么这个数就一定比另一个数小(不用管这个数后面几位有多大)),能产生多少种上升方案。然后继续考虑后面的位时,就认为这个位置恰好卡在了 \(n\) 的这一位上

并且:如果有一位比前面一位小,那么就不能满足上升了,所以如果遇到,在将所有小于该位的数字进行计算,就停止并返回当前统计得到的和。

如果到了最后都没有直接停止,那说明 \(n\) 就是一个上升好数,但前面由于最后一位也是只计算到,小于\(n\)的那一位,因此我们最后还要 +1。

那么我们所遇到的问题就是:
1、如何计算位数刚好为 \(m\) 且没有大小限制的上升好数个数?
2、如何计算倒数\(j\) 位是 \(i\) 且没有大小限制那么只考虑最后 \(j\) 位有多少种上升好数个数?

我们先考虑问题2,我们假定 up(i,j) 表示倒数\(j\) 位是 \(i\) 且没有大小限制那么只考虑最后 \(j\) 位有多少种上升好数个数。

范围:\(0 \leq i \leq 10 , 1 \leq j \leq 19\)

不难发现:\(\displaystyle{up(i,j) = \sum_{k=i}^{9}up(k,j-1)}\)
继续改进:

\[ up(i,j) = \sum_{k=i}^{9}up(k,j-1) = up(i,j-1) + \sum_{k=i+1}^{9}up(k,j-1) = up(i,j-1) + up(i+1,j); \]

注意:\(up(9,j) = 1 (1 \leq j \leq 19)\)

那么 up 数组就是:

那问题1呢?

很明显,问题1的答案(\(res1\))就是:\(\displaystyle{res1 = \sum_{k=1}^{9} up(m,k) = up(m+1,1)}\)

至此,上升就完成了。

下降(不升):

和上升的思路类似。

同样的,我们应该是首先考虑小于 \(sz\) 位的所有数字的下降方案,然后是对于每一位进行枚举,将所有小于该位的数字全部进行计算:当这个数字是倒数第 \(K\) 位时,且大小不限制(因为如果某位小于另一个数的那一位,且前面每一位两数相等,那么这个数就一定比另一个数小(不用管这个数后面几位有多大)),能产生多少种下降方案。然后继续考虑后面的位时,就认为这个位置恰好卡在了 \(n\) 的这一位上

注意:如果当前考虑的是最高位,不能考虑到0,其他位要考虑到0。

并且:如果有一位比前面一位大,那么就这次计算不能满足下降了(因为我们认为后面考虑时前面都是顶着 \(n\) 的每一位的嘛),所以如果遇到,在将所有小于该位的数字进行计算,就停止并返回当前统计得到的和。

注意,由于如果某一位发生了上升,那么我们也不能计算到这位-1,要与前面一位取min。

如果到了最后都没有直接停止,那说明 \(n\) 就是一个下降好数,但前面由于最后一位也是只计算到,小于\(n\)的那一位,因此我们最后还要 +1。

那么我们所遇到的问题就是:
1、如何计算位数刚好为 \(m\) 且没有大小限制的下降好数个数?
2、如何计算倒数\(j\) 位是 \(i\) 且没有大小限制那么只考虑最后 \(j\) 位有多少种下降好数个数?

我们先考虑问题2,我们假定 down(i,j) 表示倒数\(j\) 位是 \(i\) 且没有大小限制那么只考虑最后 \(j\) 位有多少种下降好数个数。

范围:\(0 \leq i \leq 10 , 1 \leq j \leq 19\)

不难发现:\(\displaystyle{down(i,j) = \sum_{k=0}^{i}down(k,j-1)}\)

继续改进:\(\displaystyle{down(i,j) = \sum_{k=0}^{i}down(k,j-1) = down(i,j-1) + \sum_{k=0}^{i-1}down(k,j-1) = down(i,j-1) + down(i-1,j)}\)

注意:\(down(0,j) = 1 (1 \leq j \leq 19)\)

那么down数组就完成了

那问题1呢?

很明显,问题1的答案(\(res1\))就是:\(displaystyle{res1 = \sum_{k=1}^{9} down(m,k) = down(m+1,9)-down(m,0)}\)

这里减去down(m,0)是因为不能有前导0.

至此,下降就完成了。

可是就可以做了吗?等等!

既满足上升也满足下降(即所有数字一样)

我们把所有小于等于 \(n\) 的上升数字与下降数字求出来后,我们会发现他们有一部分是重叠的,这一部分算了两遍,需要减去一遍。

计算小于等于n且所有数字一样还是比较简单的

首先是位数小于 \(sz\) 的部分:

对于每一种位数都有9种(很明显吧)。

所以先有 \(9×(sz-1)\) 种。

接着,如果位数等于 \(sz\) 且每一个数字都小于 \(n\) 的最高位,那么又有几种: \(n\)的最高位-1:

最后判断一下 \(n\) 是否所有位都不小于最高位,如果满足,那么答案再+1,否则不+1

最后的最后

最后我们说一下怎么存一些变量。

\(n\): 因为我们要枚举 \(n\) 的每一位,所以我们可以从高位到低位把 \(n\) 存到 vector 里。

\(up,down\):这两个可以用数组来存,由于需要多次使用,可以提前预处理。

还有:由于输入量极大,所以输入时建议关闭同步流,并且输出不要使用endl,用'\n',endl慢到飞起。

这样就可以做了!

3、代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;
int up[10][20];
int down[10][20];
void init()
{
    // 计算up数组
    for (int j = 1; j <= 19; j++)
    {
        up[9][j] = 1;
        for (int i = 8; i >= 0; i--)
        {
            up[i][j] = up[i + 1][j] + up[i][j - 1];
        }
    }
    // 计算down数组
    for (int j = 1; j <= 19; j++)
    {
        down[0][j] = 1;
        for (int i = 1; i <= 9; i++)
        {
            down[i][j] = down[i - 1][j] + down[i][j - 1];
        }
    }
}
pair<vector<int>, int> itov(int n) // 把n用vector存起来,
{
    vector<int> v;
    v.push_back(0); // 让vector的下标从1开始,方便后面来做。
    while (n)
    {
        v.push_back(n % 10); // 分出每一位
        n /= 10;
    }
    reverse(v.begin() + 1, v.end()); // 由于目前低位在前面,翻转一下。
    return { v, v.size() - 1 }; // 返回存好的 $n$ 和其最大下标,也就是长度,至于为什么要-1,因为还有个0在开头那里。
}
int solveup(int n) // 计算小于等于n的所有不降方案数
{
    int res = 0;
    pair<vector<int>, int> tmp = itov(n);
    vector<int> num = tmp.first; // n的vector存储
    int sz = tmp.second; // 长度
    for (int k = 1; k < sz; k++) // 位数小于sz
    {
        res += up[1][k + 1];
    }
    for (int i = 1; i <= sz; i++) // 位数等于sz
    {
        int k = sz - i + 1; // 这是倒数第 $k$ 位。
        int s = (i == 1 ? 1 : num[i - 1]); // 注意如果i == 1,从1开始就可以了。
        if (i != 1 && num[i] < num[i - 1]) // 如果不满足了递增,那么就直接返回,注意要特判一下 i != 1
        {
            return res;
        }
        for (int j = s; j < num[i]; j++) // 加上小于这一位大于等于上一位的所有方案。
        {
            res += up[j][k];
        }
    }
    return res + 1; // 记得+1
}
int solvedown(int n) // 计算小于等于n的所有不升方案数
{
    int res = 0, mn = 1e18;
    pair<vector<int>, int> tmp = itov(n);
    vector<int> num = tmp.first; // 同上
    int sz = tmp.second; // 同上
    for (int k = 1; k < sz; k++) //位数小于sz
    {
        res += down[9][k + 1] - 1;
    }
    for (int i = 1; i <= sz; i++) // 位数等于sz
    {
        int k = sz - i + 1;
        int s = (i == 1 ? num[i] - 1 : min(num[i - 1], num[i] - 1)); // 记得要取min(i == 1就别取了)
        int d = (i == 1 ? 1 : 0); // 如果不是最高位,可以考虑到0
        for (int j = s; j >= d; j--) // 加上小于这一位的所有方案。
        {
            res += down[j][k];
        }
        if (i != 1 && num[i] > num[i - 1]) // 如果不满足了递减,那么就不能继续认为全部顶着n的每一位了,后面就不能再算了。
        {
            return res;
        }
    }
    return res + 1; // 记得+1
}
int solve__(int n) // 计算小于等于n的所有数字相同的方案数
{
    int res = 0;
    pair<vector<int>, int> tmp = itov(n);
    vector<int> num = tmp.first; // 同上
    int sz = tmp.second; // 同上
    int t = (sz - 1) * 9 + num[1] - 1; // 把前两部分先加起来
    for (int i = 1; i <= sz; i++)
    {
        if (num[i] < num[1])
        {
            return t; // 如果不满足每一位都大于等于最高位,直接返回t
        }
    }
    return t + 1; // 否则返回t+1
}
void solve()
{
    int n;
    cin >> n;

    int ans = solveup(n) + solvedown(n) - solve__(n); // 上升(包括平的)+下降(包括平的)-平的
    cout << ans << '\n'; // 输出答案!用'\n'
}
signed main()
{
    ios::sync_with_stdio(false); // 优化一下
    cin.tie(0);
    cout.tie(0);
    init(); // 别忘了初始化up,down
    int t;
    cin >> t;
    while (t--)
    {
        solve();
    }
    return 0;
}

这篇题解也就这样结束了,感谢各位巨佬能够看到这里。

无耻得要赞

完结撒花!★,°:.☆( ̄▽ ̄)/$:.°★

posted @ 2025-07-16 17:45  MichaelZeng  阅读(82)  评论(0)    收藏  举报