[算法系列之三十二]1的数目

题目

Given an integer n, count the total number of digit 1 appearing in all non-negative integers less than or equal to n.

For example:
Given n = 13,
Return 6, because digit 1 occurred in the following numbers: 1, 10, 11, 12, 13.

思路一

这个问题看上去并不是一个困难的问题,因为不需要太多的思考,我想大家都能找到一个最简单的方法来计算f(N),那就是从1开始遍历到N,将其中每一个数中含有“1”的个数加起来,自然就得到了从1到N所有“1”的个数的和。

这个方法很简单,只要学过一点编程知识的人都能想到,实现也很简单,容易理解。但是这个算法的致命问题是效率,它的时间复杂度是O(N)×计算一个整数数字里面“1”的个数的复杂度 = O(N * log2 N)。如果给定的N比较大,则需要很长的运算时间才能得到计算结果。比如在笔者的机器上,如果给定N=100 000 000,则算出f(N)大概需要40秒的时间,计算时间会随着N的增大而线性增长。

看起来要计算从1到N的数字中所有1的和,至少也得遍历1到N之间所有的数字才能得到。那么能不能找到快一点的方法来解决这个问题呢?要提高效率,必须摈弃这种遍历1到N所有数字来计算f(N)的方法,而应采用另外的思路来解决这个问题。

代码一

/*---------------------------------------
*   日期:2015-07-18
*   作者:SJF0115
*   题目: 233.Number of Digit One
*   网址:https://leetcode.com/problems/number-of-digit-one/
*   结果:超时
*   来源:LeetCode
*   博客:
-----------------------------------------*/
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    int countDigitOne(int n) {
        if(n <= 9 && n >= 1){
            return 1;
        }//if
        if(n <= 0){
            return 0;
        }//if
        int result = 1;
        for(int i = 10;i <= n;++i){
            result += DigitOne(i);
        }//for
        return result;
    }
private:
    // 计算num中1的个数
    int DigitOne(int num){
        int result = 0;
        while(num){
            if(num % 10 == 1){
                ++result;
            }//if
            num /= 10;
        }//while
        return result;
    }
};

int main(){
    Solution s;
    int n;
    while(cin>>n){
        cout<<s.countDigitOne(n)<<endl;
    }//while
    return 0;
}

思路二

让我们来分析一下对于一个特定的N,如何得到一个规律来分析在每一位上所有出现1的可能性,并求和得到最后的f(N)。

先从一些简单的情况开始观察,看看能不能总结出什么规律。

先看1位数的情况。

如果N = 3,那么从1到3的所有数字:1、2、3,只有个位数字上可能出现1,而且只出现1次,进一步可以发现如果N是个位数,如果N>=1,那么f(N)都等于1,如果N=0,则f(N)为0。

再看2位数的情况。

如果N=13,那么从1到13的所有数字:1、2、3、4、5、6、7、8、9、10、11、12、13,个位和十位的数字上都可能有1,我们可以将它们分开来考虑,个位出现1的次数有两次:1和11,十位出现1的次数有4次:10、11、12和13,所以f(N)=2+4=6。要注意的是11这个数字在十位和个位都出现了1,但是11恰好在个位为1和十位为1中被计算了两次,所以不用特殊处理,是对的。再考虑N=23的情况,它和N=13有点不同,十位出现1的次数为10次,从10到19,个位出现1的次数为1、11和21,所以f(N)=3+10=13。通过对两位数进行分析,我们发现,个位数出现1的次数不仅和个位数字有关,还和十位数有关:如果N的个位数大于等于1,则个位出现1的次数为十位数的数字加1;如果N的个位数为0,则个位出现1的次数等于十位数的数字。而十位数上出现1的次数不仅和十位数有关,还和个位数有关:如果十位数字等于1,则十位数上出现1的次数为个位数的数字加1;如果十位数大于1,则十位数上出现1的次数为10。

f(13) = 个位出现1的个数 + 十位出现1的个数 = 2 + 4 = 6;
f(23) = 个位出现1的个数 + 十位出现1的个数 = 3 + 10 = 13;
f(33) = 个位出现1的个数 + 十位出现1的个数 = 4 + 10 = 14;
.........
f(93) = 个位出现1的个数 + 十位出现1的个数 = 10 + 10 = 20;

接着分析3位数。

如果N = 123:

个位出现1的个数为13:1, 11, 21, …, 91, 101, 111, 121
十位出现1的个数为20:10~19, 110~119
百位出现1的个数为24:100~123

f(23)= 个位出现1的个数 + 十位出现1的个数 + 百位出现1的次数 = 13 + 20 + 24 = 57;

同理我们可以再分析4位数、5位数。读者朋友们可以写一写,总结一下各种情况有什么不同。

根据上面的一些尝试,下面我们推导出一般情况下,从N得到f(N)的计算方法:

假设N=abcde,这里a、b、c、d、e分别是十进制数N的各个数位上的数字。如果要计算百位上出现1的次数,它将会受到三个因素的影响:百位上的数字,百位以下(低位)的数字,百位(更高位)以上的数字。

如果百位上的数字为0,则可以知道,百位上可能出现1的次数由更高位决定,比如12 013,则可以知道百位出现1的情况可能是100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,一共有1 200个。也就是由更高位数字(12)决定,并且等于更高位数字(12)×当前位数(100)

如果百位上的数字为1,则可以知道,百位上可能出现1的次数不仅受更高位影响,还受低位影响,也就是由更高位和低位共同决定。例如对于12 113,受更高位影响,百位出现1的情况是100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,一共1 200个,和上面第一种情况一样,等于更高位数字(12)×当前位数(100)。但是它还受低位影响,百位出现1的情况是12 100~12 113,一共14个,等于低位数字(13)+1

如果百位上数字大于1(即为2~9),则百位上可能出现1的次数也仅由更高位决定,比如12 213,则百位出现1的可能性为:100~199,1 100~1 199,2 100~2 199,…,11 100~11 199,12 100~12 199,一共有1 300个,并且等于更高位数字+1(12+1)×当前位数(100)

这个方法只要分析N就可以得到f(N),避开了从1到N的遍历,输入长度为Len的数字N的时间复杂度为O(Len),即为O(ln(n)/ln(10)+1)。在笔者的计算机上,计算N=100 000 000,相对于第一种方法的40秒时间,这种算法不到1毫秒就可以返回结果,速度至少提高了40 000倍。

代码二

/*---------------------------------------
*   日期:2015-07-19
*   作者:SJF0115
*   题目: 233.Number of Digit One
*   网址:https://leetcode.com/problems/number-of-digit-one/
*   结果:AC
*   来源:LeetCode
*   博客:
-----------------------------------------*/
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    int countDigitOne(int n) {
        if(n == 0){
            return 0;
        }//if
        int result = 0;
        int lowerNum = 0,curNum = 0,highNum = 0;
        int base = 1;
        int num = n;
        while(num){
            // 低位部分
            lowerNum = n - num * base;
            // 当前部分
            curNum = num % 10;
            // 高位部分
            highNum = num / 10;
            // 如果为0则这一位1出现的次数由更高位决定 (更高位数字*当前位数)
            if(curNum == 0){
                result += highNum * base;
            }//if
            // 如果为1则这一位1出现的次数不仅受更高位影响还受低位影响(更高位数字*当前位数+低位数字+1)
            else if(curNum == 1){
                result += highNum * base + (lowerNum + 1);
            }//else
            // 大于1则仅受更高位影响((更高位数字+1)*当前位数)
            else{
                result += (highNum + 1) * base;
            }//else
            num /= 10;
            base *= 10;
        }//while
        return result;
    }
};

int main(){
    Solution s;
    int n;
    while(cin>>n){
        cout<<s.countDigitOne(n)<<endl;
    }//while
    return 0;
}
时间: 2024-08-03 19:51:50

[算法系列之三十二]1的数目的相关文章

算法系列(十二) 多边形区域填充算法:几种边标志填充算法

四.边界标志填充算法 在光栅显示平面上,多边形是封闭的,它是用某一边界色围成的一 个闭合区域,填充是逐行进行的,即用扫描线逐行对多边形求交,在交点对之间填充.边界标志填充 算法就是在逐行处理时,利用边界或边界颜色作为标志来进行填充的.准确地说,边界标志填充算法 不是指某种具体的填充算法,而是一类利用扫描线连贯性思想的填充算法的总称.这类算法有很多种 ,本篇就介绍几种. 首先介绍一种以边为中心的边缘填充算法,这种边界标志算法的基本思想 是:对于每一条扫描线和每一条多边形边的交点(xi,yi),将该

算法系列(十二) 多边形区域填充算法:改进的扫描线填充算法

三.改进的扫描线填充算法 扫描线填充算法的原理和实现都很简单,但是因为要同时维护 "活动边表(AET)"和"新边表(NET)",对存储空间的要求比较高.这两张表的部分内容是重复 的,而且"新边表"在很多情况下都是一张稀疏表,如果能对其进行改进,避免出现两张表,就可以 节省存储空间,同时省去从"边表"生成"新边表"的开销,同时也省去了用"新边表"维护"活动 边表"的开销

算法系列(十二)多边形区域填充算法:扫描线填充算法(有序边表法)

二.扫描线算法(Scan-Line Filling) 扫描线算法适合对矢量图形进行区域填充,只需要 直到多边形区域的几何位置,不需要指定种子点,适合计算机自动进行图形处理的场合使用,比如电 脑游戏和三维CAD软件的渲染等等. 对矢量多边形区域填充,算法核心还是求交.<计算几何 与图形学有关的几种常用算法>一文给出了判断点与多边形关系的算法――扫描交点的奇偶数判断算 法,利用此算法可以判断一个点是否在多边形内,也就是是否需要填充,但是实际工程中使用的填充 算法都是只使用求交的思想,并不直接使用这

[算法系列之十二]字符串匹配之蛮力匹配

引言 字符串匹配是数据库开发和文字处理软件的关键.幸运的是所有现代编程语言和字符串库函数,帮助我们的日常工作.不过理解他们的原理还是比较重要的. 字符串算法主要可以分为几类.字符串匹配就是其中之一.当我们提到字符串匹配算法,最基本的方法就是所谓的蛮力解法,这意味着我们需要检查每一个文本串中的字符是否和匹配串相匹配.一般来说我们有文本串和一个匹配串(通常匹配串短于文本串).我们需要做的就是回答这个匹配串是否出现在文本串中. 概述 字符串蛮力匹配法的原理非常简单.我们必须检查匹配串的第一个字符与文本

[算法系列之三十]Dijkstra单源最短路径算法

单源最短路径问题 给定一个带权有向图 G=(V,E) ,其中每条边的权是一个非负实数.另外,还给定 V 中的一个顶点,称为源.现在我们要计算从源到所有其他各顶点的最短路径长度.这里的长度是指路上各边权之和.这个问题通常称为单源最短路径问题. 前面Bellman-Ford最短路径算法讲了单源最短路径的Bellman-Ford算法(动态规划算法).这里介绍另外一个更常见的算法Dijkstra算法. Dijkstra算法和 最小生成树Prim算法最小生成树算法非常类似,大家可以先熟悉下个算法.两个算法

算法系列(十二) 多边形区域填充算法--扫描线种子填充算法

1.3扫描线种子填充算法 1.1和1.2节介绍的两种种子填充算法的优点是非常简单,缺点是使 用了递归算法,这不但需要大量栈空间来存储相邻的点,而且效率不高.为了减少算法中的递归调用 ,节省栈空间的使用,人们提出了很多改进算法,其中一种就是扫描线种子填充算法.扫描线种子填 充算法不再采用递归的方式处理"4-联通"和"8-联通"的相邻点,而是通过沿水平扫描线填充像素 段,一段一段地来处理"4-联通"和"8-联通"的相邻点.这样算法

算法系列(十二) 多边形区域填充算法--递归种子填充算法

平面区域填充算法是计算机图形学领域的一个很重要的算法,区域填充即给出一个区域的边界(也 可以是没有边界,只是给出指定颜色),要求将边界范围内的所有象素单元都修改成指定的颜色(也 可能是图案填充).区域填充中最常用的是多边形填色,本文中我们就讨论几种多边形区域填充算法 . 一.种子填充算法(Seed Filling) 如果要填充的区域是以图像元数据方式给出的 ,通常使用种子填充算法(Seed Filling)进行区域填充.种子填充算法需要给出图像数据的区域, 以及区域内的一个点,这种算法比较适合人

剑指offer系列之三十二:寻找丑数

题目描述 把只包含因子2.3和5的数称作丑数(Ugly Number).例如6.8都是丑数,但14不是,因为它包含因子7. 习惯上我们把1当做是第一个丑数.求按从小到大的顺序的第N个丑数. 因为丑数只有2.3和5这三个因子,那么如果一个是丑数的话,一定是可以被这个三个因子整除的,所以我们可以通过把一个数一直被三个因子除,这样最后如果该数变成1的话(因为第一个丑数是1),那么就验证了该数就是丑数.那么如果想找出第k个丑数的话,就可以从第一个数开始循环,对每一个数进行丑数的判断,从而找到符合要求的第

[算法系列之十四]字符串匹配之Morris-Pratt字符串搜索算法

前言 我们前面已经看到,蛮力字符串匹配算法和Rabin-Karp字符串匹配算法均非有效算法.不过,为了改进某种算法,首先需要详细理解其基本原理.我们已经知道,暴力字符串匹配的速度缓慢,并已尝试使用Rabin-Karp中的一个散列函数对其进行改进.问题是,Rabin-Karp的复杂度与强力字符串匹配相同,均为O(mn). 我们显然需要采用一种不同方法,但为了提出这种不同方法,先来看看暴力字符串匹配有什么不妥之处.事实上,再深入地研究一下它的基本原理,就能找到问题的答案了. 在暴力匹配算法中,需要检