常规暴力
肯定是通不过的,数据太多了,将所有数据存进一个列表然后排序找到第k个是最直观的思路,但是不行,这限制了我之后的想法,一直想着先处理好数据,先对乘法表中的数据进行啥样的操作然后一下找出对应的结果,这样是写不出来的
二分法
二分是我刚接触的算法,它是反的,像是暴力循环一样,其实就是暴力循环,只不过每次取一半把数据变少了。
拿
1 2
2 4 k=3
这个乘法表举例子,首先创建一个1234这样的列表,然后从1开始,这个表中小于等于1的数有1个(注意,如何判断一个乘法表中小于等于某个数有几个可以用下面的help函数)
def help(m, n, mid):
cout = 0
# 一行一行的判断,这一行的每一个数都是这一行行号的倍数
# 如果//行号>n表示这一行的所有数都比mid小,+n
for i in range(1, m+1):
cout += min(mid//i, n)
return cout
1<k所以继续往后面走,到2,表中<=2的数有3个刚好,所以答案就是2,二分法就是这样的思路但是它不是从头到尾一个一个走,而是二分跳跃着找所以当数据特别多时也会很快,其实这就像暴力一样,反正正确答案一定在1-m*n之间,就在这里面一个个找呗,这就是二分与之前解法的不同之处!不是一步得到答案而是一个个试出来的!
二分代码:
def findKthNumber(self, m: int, n: int, k: int) -> int:
# 找到m*n乘法表中<=mid的数的个数
def help(m, n, mid):
cout = 0
for i in range(1, m+1):
cout += min(mid//i, n)
return cout
# 二分套路
l=0
r=m*n+1
while l+1 != r:
middle = (l+r)//2
if help(m, n, middle) >= k:
r = middle
else:
l = middle
# 最后l和r就差1,返回l+1还是r其实都一个样
return r
这里有几个问题:
1、为什么二分遍历的是1-m*n这样一个连续的列表但是结果却总是一个在乘法表中的数?
首先所有的数都是整数(其实就算有小数也不从根本上影响),拿上面的22的乘法表举例,假如好巧不巧到了3,表中<=3的数和<=2的数一模一样都是3个,会不会返回的结果是3呢,答案是不会,因为如果遍历到了3那么这个二分就没有结束,3前面的2也满足条件自然会继续移动到2,所有二分都会一直进行直到前一个不满足才会结束二分,判断一个mn乘法表中有多少数<mid的这个函数从某种程度上决定了最符合要求的数一定是在这个表中的数
2、假如上面的k=2,是不是就出不来结果?
不会,虽然表中<=2的数有3个 3>k=2 ,但是遍历过程中,2仍然是最符合要求的,遍历到4时>help()=4会向前走到help()3=3会继续走到help()2=3仍然会向前走但是help()1=1<2了就会停止(停止的原因不是help()1=1<2而是help()2>2 help()1又<2, l和r之间没得走了只能退出二分),这个时候左指针 l 指向1-m*n这个列表中最后一个在乘法表中<=个数小于k的数,而r指针则相反,指向的是表中<=自身的数的个数第一次>=k的数,等于自然可以理解但是>也是可以的,因为它是第一个