前置知识:时间复杂度全排列递归代码的执行细节

一个基本事实

C/C++ 运行时间 1s,Java/Python/Go 等其他语言运行时间 1s~2s,对应的常数指令操作量(常数操作)是 ,不管什么测试平台,不管什么 cpu,都是这个数量级

一般来说,算法的常数操作量大于 ,即 C/C++ 运行时间超过 1s,测试平台会报“时间超出限制”而不通过

所以可以根据这个基本事实,来猜测自己设计的算法最终有没有可能在规定时间内通过

运用根据数据量猜解法技巧的必要条件:

  1. 题目要给定各个入参的范围最大值,正式笔试、比赛的题目一定都会给,面试中要和面试官确认
  2. 对于自己设计的算法,时间复杂度要有准确的估计

运用根据数据量猜解法技巧的说明:

  • 该技巧只保证能够通过题目,不一定是最优解,常用于答题时间紧张的笔试、比赛中
  • 该技巧既可以提前获知自己的方法能不能通过,也可以对题目的分析有引导作用

问题规模和可用算法

YesYesYesYesYesYesYes
YesYesYesYesYesYesNo
YesYesYesYesYesNoNo
YesYesYesYesNoNoNo
YesYesYesNoNoNoNo
YesYesNoNoNoNoNo
YesNoNoNoNoNoNo

上面每个复杂度,课上都讲过类似的过程了

除了 ,这个复杂度常出现在“莫队算法”能解决的相关题目里,后续的【挺难】课程会有系统讲述

质数筛选问题也是

这张表其实作用有限,因为时间复杂度的估计很多时候并不是一个入参决定,可能是多个入参共同决定。比如

所以最关键的就是记住常数指令操作量是 ,然后方法是什么复杂度就可以估计能否通过了

题目1.最优的技能释放顺序

题目描述

现在有一个打怪类型的游戏,这个游戏是这样的,你有 n 个技能,每一个技能会有一个伤害,同时若怪物低于一定的血量,则该技能可能造成双倍伤害,每一个技能最多只能释放一次,已知怪物有 m 点血量,现在想问你最少用几个技能能消灭掉他(血量小于等于 0)

数据量:

测试链接

消灭怪物

思路

,所以将技能全排列()也能通过时间限制

答案

牛客这道题没有 Go 环境,用 Java 吧

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
 
public class Main {
    public static int MAXN = 11;
    // 技能可以打多少血
    public static int[] kill = new int[MAXN];
    // 双倍伤害血量阈值
    public static int[] blood = new int[MAXN];
    // 一共有几个技能
    public static int n = 0;
 
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            int t = (int)in.nval;
            for (int i = 0; i < t; i++) {
                in.nextToken();
                n = (int)in.nval;
                in.nextToken();
                int m = (int)in.nval;
                for (int j = 0; j < n; j++) {
                    in.nextToken();
                    kill[j] = (int)in.nval;
                    in.nextToken();
                    blood[j] = (int)in.nval;
                }
                int ans = f(0, m);
                out.println(ans == Integer.MAX_VALUE ? -1 : ans);
            }
        }
        out.flush();
        br.close();
        out.close();
    }
 
    // idx: 当前来到了第几号技能
    // rest: 怪兽目前的剩余血量
    public static int f(int idx, int rest) {
        if (rest <= 0) {
            // 之前的决策已经让怪兽挂了!返回使用了多少个技能
            return idx;
        }
        if (idx == n) {
            // 没打死,之前的决策无效
            return Integer.MAX_VALUE;
        }
 
        // 返回至少需要几个技能可以将怪兽杀死
        int ans = Integer.MAX_VALUE;
        for (int i = idx; i < n; i++) {
            swap(i, idx);
            ans = Math.min(ans, f(idx + 1,
                                  rest - (rest > blood[idx] ? kill[idx] : 2 * kill[idx])));
            swap(i, idx);
        }
        return ans;
    }
 
    public static void swap(int i, int j) {
        int tmp = kill[i];
        kill[i] = kill[j];
        kill[j] = tmp;
        tmp = blood[i];
        blood[i] = blood[j];
        blood[j] = tmp;
    }
}

题目2.超级回文数的数目

题目描述

如果一个正整数自身是回文数,而且它也是一个回文数的平方,那么我们称这个数为超级回文数。

现在,给定两个正整数 L 和 R (以字符串形式表示),返回包含在范围 [L, R] 中的超级回文数的数目。

示例:

输入:L = "4", R = "1000"

输出:4

解释:4,9,121,以及 484 是超级回文数。注意 676 不是一个超级回文数: 26 * 26 = 676,但是 26 不是回文数

提示:

  1. 1 <= len(L) <= 18
  2. 1 <= len(R) <= 18
  3. L 和 R 是表示 [1, 10^18) 范围的整数的字符串。
  4. int(L) <= int(R)

测试链接

906.超级回文数

思路

由题目数据量可以看到,一个一个遍历的话,常数操作是 规模的,这必然超时

一个数 是超级回文数的话, 也必须是回文数,所以可以遍历 的回文数看它的平方在不在 [L, R] 范围内,这样就降到了 规模,还是超时

其实只需要看 的前一半字符即可获得 ,如:前一半字符是 123,则 可以是 12321123321,这样就降到了 规模

答案

func superpalindromesInRange(left string, right string) int {
	ans := 0
	l, _ := strconv.Atoi(left)
	r, _ := strconv.Atoi(right)
	// 最大根号 x,数据规模 10^9
	limit := int(math.Sqrt(float64(r)))
	// 根号 x 的前一半字符,数据规模 10^5
	seed := 1
	// 根号 x
	num := 0
	for num < limit {
		num = evenEnlarge(seed)
		if check(num*num, l, r) {
			ans++
		}
		num = oddEnlarge(seed)
		if check(num*num, l, r) {
			ans++
		}
		seed++
	}
	return ans
}
 
// 放大到偶数回文串
// 123 -> 123321
func evenEnlarge(seed int) int {
	ans := seed
	for seed != 0 {
		ans = ans*10 + seed%10
		seed /= 10
	}
	return ans
}
 
// 放大到奇数回文串
// 123 -> 12321
func oddEnlarge(seed int) int {
	ans := seed
	seed /= 10
	for seed != 0 {
		ans = ans*10 + seed%10
		seed /= 10
	}
	return ans
}
 
func check(x, l, r int) bool {
	return x >= l && x <= r && isPalindrome(x)
}
 
// 验证 x 是不是回文数字
func isPalindrome(x int) bool {
	offset := 1
	// 注意这么写是为了防止溢出
	for x/offset >= 10 {
		offset *= 10
	}
 
	for x != 0 {
		if x/offset != x%10 {
			return false
		}
		x = (x % offset) / 10
		offset /= 100
	}
	return true
}

解释

evenEnlarge

// 放大到偶数回文串
// 123 -> 123321
func evenEnlarge(seed int) int {
	ans := seed
	for seed != 0 {
		ans = ans*10 + seed%10
		seed /= 10
	}
	return ans
}
seed = 123

ans = 123 * 10 + 123 % 10 = 1233, seed = 12
ans = 1233 * 10 + 12 % 10 = 12332, seed = 1
ans = 12332 * 10 + 1 % 10 = 123321, seed = 0

oddEnlarge 同理

isPalindrome

是单独的一道题:回文数

打表的方法

用上面的方法看下整个 int64 范围内所有的超级回文数

package main
 
import (
	"fmt"
	"math"
	"sort"
)
 
func main() {
	superpalindromes := collectSuperpalindromes()
	sort.Slice(superpalindromes, func(i, j int) bool {
		return superpalindromes[i] < superpalindromes[j]
	})
	fmt.Println(len(superpalindromes)) // 86
	// fmt.Println(superpalindromes)
}
 
// 收集超级回文数
func collectSuperpalindromes() []int64 {
	var (
		l    int64 = 1
		r    int64 = math.MaxInt64
		seed int64 = 1
		num  int64 = 0
	)
	superpalindromes := []int64{}
	limit := int64(math.Sqrt(float64(r)))
	for num < limit {
		num = evenEnlarge(seed)
		if check(num*num, l, r) {
			superpalindromes = append(superpalindromes, num*num)
		}
		num = oddEnlarge(seed)
		if check(num*num, l, r) {
			superpalindromes = append(superpalindromes, num*num)
		}
		seed++
	}
	return superpalindromes
}
 
// 放大到偶数回文串
// 123 -> 123321
func evenEnlarge(seed int64) int64 {
	ans := seed
	for seed != 0 {
		ans = ans*10 + seed%10
		seed /= 10
	}
	return ans
}
 
// 放大到奇数回文串
// 123 -> 12321
func oddEnlarge(seed int64) int64 {
	ans := seed
	seed /= 10
	for seed != 0 {
		ans = ans*10 + seed%10
		seed /= 10
	}
	return ans
}
 
func check(x, l, r int64) bool {
	return x >= l && x <= r && isPalindrome(x)
}
 
// 验证 x 是不是回文数字
func isPalindrome(x int64) bool {
	var offset int64 = 1
	// 注意这么写是为了防止溢出
	for x/offset >= 10 {
		offset *= 10
	}
 
	for x != 0 {
		if x/offset != x%10 {
			return false
		}
		x = (x % offset) / 10
		offset /= 100
	}
	return true
}
 

发现 int64 范围内只有 86 个超级回文数,直接 打表求解,时间复杂度 ,无敌快!

var (
	record = [...]int{1, 4, 9, 121, 484, 10201, 12321, 14641, 40804, 44944, 1002001, 1234321, 4008004, 100020001, 102030201, 104060401, 121242121, 123454321, 125686521, 400080004, 404090404, 10000200001, 10221412201, 12102420121, 12345654321, 40000800004, 1000002000001, 1002003002001, 1004006004001, 1020304030201, 1022325232201, 1024348434201, 1210024200121, 1212225222121, 1214428244121, 1232346432321, 1234567654321, 4000008000004, 4004009004004, 100000020000001, 100220141022001, 102012040210201, 102234363432201, 121000242000121, 121242363242121, 123212464212321, 123456787654321, 400000080000004, 10000000200000001, 10002000300020001, 10004000600040001, 10020210401202001, 10022212521222001, 10024214841242001, 10201020402010201, 10203040504030201, 10205060806050201, 10221432623412201, 10223454745432201, 12100002420000121, 12102202520220121, 12104402820440121, 12122232623222121, 12124434743442121, 12321024642012321, 12323244744232321, 12343456865434321, 12345678987654321, 40000000800000004, 40004000900040004, 1000000002000000001, 1000220014100220001, 1002003004003002001, 1002223236323222001, 1020100204020010201, 1020322416142230201, 1022123226223212201, 1022345658565432201, 1210000024200000121, 1210242036302420121, 1212203226223022121, 1212445458545442121, 1232100246420012321, 1232344458544432321, 1234323468643234321, 4000000008000000004}
)
 
func superpalindromesInRange(left string, right string) int {
	l, _ := strconv.Atoi(left)
	r, _ := strconv.Atoi(right)
	n := len(record)
 
	// 二分都懒得用
	i := 0
	for ; i < n; i++ {
		if record[i] >= l {
			break
		}
	}
	j := n - 1
	for ; j >= 0; j-- {
		if record[j] <= r {
			break
		}
	}
	return j - i + 1
}

题目3.回文数

题目描述

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是

测试链接

9.回文数

思路一

// 验证 x 是不是回文数字
func isPalindrome(x int) bool {
	if x < 0 {
		return false
	}
	offset := 1
	// 注意这么写是为了防止溢出
	for x/offset >= 10 {
		offset *= 10
	}
 
	for x != 0 {
		if x/offset != x%10 {
			return false
		}
		x = (x % offset) / 10
		offset /= 100
	}
	return true
}
x = 123321

offset = 100000

x/offset = 123321/100000 = 1 // 提取最左侧数字(最高位)
x/10 = 123321/10 = 1         // 提取最右侧数字(最低位)

x = (x % offset) / 10
  = 23321 / 10
  = 2332                     // 去除最高位和最低位


offset /= 100 
  = 100000/100
  = 1000                     // 去除两位,保持和 x 位数一样

思路二

把数字逆序,然后和原数比较

func isPalindrome(x int) bool {
	if x < 0 {
		return false
	}
	reversed := 0
	for i := x; i != 0; i /= 10 {
		reversed = reversed*10 + i%10
	}
	return reversed == x
}

该方法能通过测试,但是有个问题:如果 x 已经确定不是回文数,而且它逆序的结果会溢出。有没有可能存在某个 x,逆序溢出之后得到的结果还是 x,导致判断错误?如果没有这种可能,需要证明;而 思路一 一定不溢出,一定是正确的。

思路三

intstring 后左右双指针