42.【必备】根据数据量猜解法的技巧

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解043【必备】根据数据量猜解法的技巧-天字第一号重要技巧_哔哩哔哩_bilibili

一.问题规模和可用算法

一个基本事实

C/C++运行时间1s,java/python/go等其他语言运行时间1s~2s,

对应的常数指令操作量是 10^7 ~ 10^8,不管什么测试平台,不管什么cpu,都是这个数量级

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

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

1,题目要给定各个入参的范围最大值,正式笔试、比赛的题目一定都会给,面试中要和面试官确认

2,对于自己设计的算法,时间复杂度要有准确的估计

这个技巧太重要了!既可以提前获知自己的方法能不能通过,也可以对题目的分析有引导作用!

问题规模和可用算法

lognnn*lognn*根号nn^22^nn!
n <= 11          YesYesYesYesYesYesYes
n <= 25YesYesYesYesYesYes

No

n <= 5000        YesYesYesYesYes

No

No

n <= 10^5        YesYesYesYes

No

No

No

n <= 10^6        YesYesYes

No

No

No

No

n <= 10^7        YesYes

No

No

No

No

No

n >= 10^8        Yes

No

No

No

No

No

No

                

这张表其实作用有限

因为时间复杂度的估计很多时候并不是一个入参决定,可能是多个入参共同决定。比如O(n*m), O(n+m)等

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

二.消灭怪物

题目:消灭怪物

算法原理

  • 整体原理
    • 这是一个基于深度优先搜索(DFS)策略的算法,目的是在给定的技能组合和怪物血量的情况下,找到使用最少的技能数量来消灭怪物。通过对技能使用顺序的全排列搜索,计算每种顺序下消灭怪物所需的技能数量,最终找到最小值。
  • 具体步骤
    • 输入处理(main方法部分)
      • 读取测试用例数量:使用BufferedReaderStreamTokenizer读取输入。首先读取一个整数t,表示测试用例的数量。
      • 处理每个测试用例:对于每个测试用例,先读取技能数量n和怪物血量m。然后通过循环(for (int j = 0; j < n; j++))依次读取每个技能的伤害值kill[j]和触发双倍伤害的血量最小值blood[j]
      • 调用计算函数并输出结果:调用f函数计算消灭怪物所需的最少技能数量,将结果存储在ans中。如果ans等于Integer.MAX_VALUE,说明没有找到可行的技能组合来消灭怪物,输出 - 1;否则输出ans。最后,刷新PrintWriter并关闭BufferedReaderPrintWriter以释放资源。
    • 核心计算(f函数部分)
      • 递归基情况(终止条件)
        • 成功消灭怪物:当怪物的剩余血量r小于等于0时,表示已经成功消灭怪物,直接返回当前已经使用的技能数量i
        • 技能用尽仍未消灭怪物:当已经尝试了所有的技能(i等于技能总数n),但怪物的血量r仍然大于0,说明之前的技能使用顺序无法消灭怪物,返回Integer.MAX_VALUE
      • 递归搜索过程
        • 首先初始化ansInteger.MAX_VALUE,表示当前找到的最少技能数量(初始假设还未找到可行解)。
        • 然后通过循环(for (int j = i; j < n; j++))尝试不同的技能顺序。在每次循环中,先将第j个技能和第i个技能交换位置(通过swap函数),这相当于尝试先使用第j个技能的情况。
        • 接着计算使用第i个技能后的怪物剩余血量。根据怪物当前血量r与技能i触发双倍伤害的血量最小值blood[i]的关系,确定造成的伤害。如果r > blood[i],则造成的伤害为kill[i];否则为kill[i]*2。然后递归调用f函数(f(n, i + 1, r-(r > blood[i]? kill[i] : kill[i]*2))),这个递归调用表示继续尝试下一个技能(i+1),并根据当前技能造成的伤害更新怪物的剩余血量。
        • Math.min函数不断更新ans,使其始终保存当前找到的最少技能数量。最后,再将交换后的技能位置换回来(再次调用swap函数),以便继续尝试其他可能的技能组合。
    • 技能交换(swap函数部分)
      • 这个函数的作用是交换两个技能的相关信息,包括技能的伤害值kill和触发双倍伤害的血量最小值blood。它接受两个索引ij作为参数,通过临时变量tmp,先交换kill[i]kill[j],再交换blood[i]blood[j],从而实现技能信息的交换。这一步操作是为了在递归搜索中模拟不同的技能使用顺序。

代码实现

// 现在有一个打怪类型的游戏,这个游戏是这样的,你有n个技能
// 每一个技能会有一个伤害,
// 同时若怪物小于等于一定的血量,则该技能可能造成双倍伤害
// 每一个技能最多只能释放一次,已知怪物有m点血量
// 现在想问你最少用几个技能能消灭掉他(血量小于等于0)
// 技能的数量是n,怪物的血量是m
// i号技能的伤害是x[i],i号技能触发双倍伤害的血量最小值是y[i]
// 1 <= n <= 10
// 1 <= m、x[i]、y[i] <= 10^6
// 测试链接 : https://www.nowcoder.com/practice/d88ef50f8dab4850be8cd4b95514bbbd
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的所有代码,并把主类名改成"Main"
// 可以直接通过

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 Code01_KillMonsterEverySkillUseOnce {

    public static int MAXN = 11;

    public static int[] kill = new int[MAXN];

    public static int[] blood = new int[MAXN];

    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();
                int 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(n, 0, m);
                out.println(ans == Integer.MAX_VALUE ? -1 : ans);
            }
        }
        out.flush();
        br.close();
        out.close();
    }

    // kill[i]、blood[i]
    // n : 一共几个技能
    // i : 当前来到了第几号技能
    // r : 怪兽目前的剩余血量
    public static int f(int n, int i, int r) {
        if (r <= 0) {
            // 之前的决策已经让怪兽挂了!返回使用了多少个节能
            return i;
        }
        // r > 0
        if (i == n) {
            // 无效,之前的决策无效
            return Integer.MAX_VALUE;
        }
        // 返回至少需要几个技能可以将怪兽杀死
        int ans = Integer.MAX_VALUE;
        for (int j = i; j < n; j++) {
            swap(i, j);
            ans = Math.min(ans, f(n, i + 1, r - (r > blood[i] ? kill[i] : kill[i] * 2)));
            swap(i, j);
        }
        return ans;
    }

    // i号技能和j号技能,参数交换
    // j号技能要来到i位置,试一下
    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;
    }

}

三.超级回文数

题目:超级回文数

算法原理

  • 整体原理
    • 这个算法主要是为了找出给定范围[L, R]中的超级回文数的数目。超级回文数是一个正整数自身是回文数,而且它也是一个回文数的平方。算法通过两种方式来实现:一种是通过枚举可能的回文数的根号值(num),计算其平方并检查是否在范围内且是回文数;另一种是通过打表的方式(superpalindromesInRange2方法),利用预定义的超级回文数数组record来快速计算范围内的超级回文数数量。
  • 具体步骤
    • 第一步:数据初始化。
      • 将输入的字符串表示的范围边界leftright转换为long类型,分别赋值给lr
      • 计算范围上限r的平方根,得到limit,这个值用于限制枚举的范围。
      • 初始化枚举种子seed = 1,用于生成可能的回文数的根号值。初始化num = 0ans = 0ans用于记录超级回文数的数量)。
    • 第二步:枚举可能的回文数的根号值。
      • 进入do - while循环。
      • 首先通过evenEnlarge方法根据种子seed生成偶数长度的回文数字作为可能的num(例如,seed = 123,则生成123321)。
      • 然后调用check方法,检查num的平方是否在[l, r]范围内并且是回文数,如果是则ans加1。
      • 接着通过oddEnlarge方法根据种子seed生成奇数长度的回文数字作为可能的num(例如,seed = 123,则生成12321)。
      • 再次调用check方法,检查num的平方是否在[l, r]范围内并且是回文数,如果是则ans加1。
      • 最后将seed加1,继续下一轮循环,直到num大于等于limit
    • 第三步:生成偶数长度回文数(evenEnlarge方法)。
      • 接受一个long类型的种子seed
      • 初始化ans = seed
      • 通过while (seed!= 0)循环,每次将ans乘以10并加上seed的最低位(seed % 10),然后将seed除以10。例如,seed = 123,第一次循环ans = 123seed = 12;第二次循环ans = 1233seed = 1;第三次循环ans = 12332seed = 0;最终ans = 123321
    • 第四步:生成奇数长度回文数(oddEnlarge方法)。
      • 接受一个long类型的种子seed
      • 先将seed除以10(去掉最低位),初始化ans = seed
      • 通过while (seed!= 0)循环,每次将ans乘以10并加上seed的最低位(seed % 10),然后将seed除以10。例如,seed = 123,第一次seed = 12ans = 12;第二次seed = 1ans = 123;最终ans = 12321
    • 第五步:检查是否符合超级回文数条件(check方法)。
      • 接受一个数ans以及范围边界lr
      • 首先检查ans是否在[l, r]范围内(ans >= l && ans <= r),然后调用isPalindrome方法检查ans是否为回文数,如果两个条件都满足则返回true
    • 第六步:检查是否为回文数(isPalindrome方法,与之前的回文数检查类似)。
      • 接受一个long类型的数字num
      • 初始化offset = 1,通过while (num / offset >= 10)循环,不断将offset乘以10,直到num除以offset小于10,以确定数字的最高位权重。
      • 然后通过while (num!= 0)循环,比较num的最高位(num / offset)和最低位(num % 10),如果不相等则返回false,如果相等则对num进行调整(num=(num%offset)/10,同时offset /= 100),直到num变为0,此时返回true
    • 第七步:打表方法(superpalindromesInRange2方法)。
      • 将输入的字符串表示的范围边界leftright转换为long类型,分别赋值给lr
      • 通过两个for循环,在预定义的超级回文数数组record中找到第一个大于等于l的数的下标i和最后一个小于等于r的数的下标j
      • 返回j - i + 1,即范围内超级回文数的数量。

代码实现

import java.util.ArrayList;
import java.util.List;

// 超级回文数(java版)
// 如果一个正整数自身是回文数,而且它也是一个回文数的平方,那么我们称这个数为超级回文数。
// 现在,给定两个正整数 L 和 R (以字符串形式表示),
// 返回包含在范围 [L, R] 中的超级回文数的数目。
// 1 <= len(L) <= 18
// 1 <= len(R) <= 18
// L 和 R 是表示 [1, 10^18) 范围的整数的字符串
//测试链接 : https://leetcode.cn/problems/super-palindromes/
public class Code02_SuperPalindromes {

    // [left, right]有多少超级回文数
    // 返回数量
    public static int superpalindromesInRange1(String left, String right) {
        long l = Long.valueOf(left);
        long r = Long.valueOf(right);
        // l....r  long
        // x根号,范围limit
        long limit = (long) Math.sqrt((double) r);
        // seed : 枚举量很小,10^18 -> 10^9 -> 10^5
        // seed : 奇数长度回文、偶数长度回文
        long seed = 1;
        // num : 根号x,num^2 -> x
        long num = 0;
        int ans = 0;
        do {
            //  seed生成偶数长度回文数字
            // 123 -> 123321
            num = evenEnlarge(seed);
            if (check(num * num, l, r)) {
                ans++;
            }
            //  seed生成奇数长度回文数字
            // 123 -> 12321
            num = oddEnlarge(seed);
            if (check(num * num, l, r)) {
                ans++;
            }
            // 123 -> 124 -> 125
            seed++;
        } while (num < limit);
        return ans;
    }

    // 根据种子扩充到偶数长度的回文数字并返回
    public static long evenEnlarge(long seed) {
        long ans = seed;
        while (seed != 0) {
            ans = ans * 10 + seed % 10;
            seed /= 10;
        }
        return ans;
    }

    // 根据种子扩充到奇数长度的回文数字并返回
    public static long oddEnlarge(long seed) {
        long ans = seed;
        seed /= 10;
        while (seed != 0) {
            ans = ans * 10 + seed % 10;
            seed /= 10;
        }
        return ans;
    }

    // 判断ans是不是属于[l,r]范围的回文数
    public static boolean check(long ans, long l, long r) {
        return ans >= l && ans <= r && isPalindrome(ans);
    }

    // 验证long类型的数字num,是不是回文数字
    public static boolean isPalindrome(long num) {
        long offset = 1;
        // 注意这么写是为了防止溢出
        while (num / offset >= 10) {
            offset *= 10;
        }
        // num    : 52725
        // offset : 10000
        // 首尾判断
        while (num != 0) {
            if (num / offset != num % 10) {
                return false;
            }
            num = (num % offset) / 10;
            offset /= 100;
        }
        return true;
    }

    // 打表的方法
    // 必然最优解
    // 连二分都懒得用
    public static int superpalindromesInRange2(String left, String right) {
        long l = Long.parseLong(left);
        long r = Long.parseLong(right);
        int i = 0;
        for (; i < record.length; i++) {
            if (record[i] >= l) {
                break;
            }
        }
        int j = record.length - 1;
        for (; j >= 0; j--) {
            if (record[j] <= r) {
                break;
            }
        }
        return j - i + 1;
    }

    public static long[] record = new long[] {
            1L,
            4L,
            9L,
            121L,
            484L,
            10201L,
            12321L,
            14641L,
            40804L,
            44944L,
            1002001L,
            1234321L,
            4008004L,
            100020001L,
            102030201L,
            104060401L,
            121242121L,
            123454321L,
            125686521L,
            400080004L,
            404090404L,
            10000200001L,
            10221412201L,
            12102420121L,
            12345654321L,
            40000800004L,
            1000002000001L,
            1002003002001L,
            1004006004001L,
            1020304030201L,
            1022325232201L,
            1024348434201L,
            1210024200121L,
            1212225222121L,
            1214428244121L,
            1232346432321L,
            1234567654321L,
            4000008000004L,
            4004009004004L,
            100000020000001L,
            100220141022001L,
            102012040210201L,
            102234363432201L,
            121000242000121L,
            121242363242121L,
            123212464212321L,
            123456787654321L,
            400000080000004L,
            10000000200000001L,
            10002000300020001L,
            10004000600040001L,
            10020210401202001L,
            10022212521222001L,
            10024214841242001L,
            10201020402010201L,
            10203040504030201L,
            10205060806050201L,
            10221432623412201L,
            10223454745432201L,
            12100002420000121L,
            12102202520220121L,
            12104402820440121L,
            12122232623222121L,
            12124434743442121L,
            12321024642012321L,
            12323244744232321L,
            12343456865434321L,
            12345678987654321L,
            40000000800000004L,
            40004000900040004L,
            1000000002000000001L,
            1000220014100220001L,
            1002003004003002001L,
            1002223236323222001L,
            1020100204020010201L,
            1020322416142230201L,
            1022123226223212201L,
            1022345658565432201L,
            1210000024200000121L,
            1210242036302420121L,
            1212203226223022121L,
            1212445458545442121L,
            1232100246420012321L,
            1232344458544432321L,
            1234323468643234321L,
            4000000008000000004L
    };

    public static List<Long> collect() {
        long l = 1;
        long r = Long.MAX_VALUE;
        long limit = (long) Math.sqrt((double) r);
        long seed = 1;
        long enlarge = 0;
        ArrayList<Long> ans = new ArrayList<>();
        do {
            enlarge = evenEnlarge(seed);
            if (check(enlarge * enlarge, l, r)) {
                ans.add(enlarge * enlarge);
            }
            enlarge = oddEnlarge(seed);
            if (check(enlarge * enlarge, l, r)) {
                ans.add(enlarge * enlarge);
            }
            seed++;
        } while (enlarge < limit);
        ans.sort((a, b) -> a.compareTo(b));
        return ans;
    }

    public static void main(String[] args) {
        List<Long> ans = collect();
        for (long p : ans) {
            System.out.println(p + "L,");
        }
        System.out.println("size : " + ans.size());
    }

}

四.回文数

题目:回文数

算法原理

  • 整体原理
    • 这个算法主要是通过比较数字的首位和末位,然后逐步向内比较数字的对称部分来判断一个数是否为回文数。对于负数,直接判定不是回文数。对于非负数,通过巧妙地确定数字的最高位权重(通过offset),然后在循环中不断比较并去掉已经比较过的数字对,直到所有数字都比较完或者发现不对称的情况。
  • 具体步骤
    • 第一步:处理负数情况。
      • 如果num < 0,直接返回false。因为回文数不能是负数。
    • 第二步:确定数字的位数(计算offset)。
      • 初始化offset = 1
      • 进入while (num / offset >= 10)循环,不断将offset乘以10,直到num除以offset的结果小于10。例如,对于123,第一次num / offset为123 / 1 = 123,满足条件,offset变为10;第二次num / offset为123 / 10 = 12.3,满足条件,offset变为100;第三次num / offset为123 / 100 = 1.23,不满足条件,此时offset = 100
    • 第三步:首尾数字比较循环。
      • 进入while (num!= 0)循环。
      • 在循环中,首先比较num的最高位(num/offset)和最低位(num%10)。例如对于121,num/offset为121/100 = 1,num%10为121%10 = 1,两者相等。
      • 如果不相等,直接返回false
      • 如果相等,对num进行调整,num=(num%offset)/10,这一步去掉了已经比较过的首位和末位数字。例如对于121,num变为(121%100)/10 = 2。同时,offset除以100,因为已经去掉了两位数字,offset从100变为1。
      • 不断重复这个过程,直到num变为0,此时返回true

代码实现

// 超级回文数中的一个小函数,本身也是一道题 : 判断一个数字是不是回文数
// 测试链接 : https://leetcode.cn/problems/palindrome-number/
public class Code03_IsPalindrome {

    public static boolean isPalindrome(int num) {
        if (num < 0) {
            return false;
        }
        int offset = 1;
        // 注意这么写是为了防止溢出
        while (num / offset >= 10) {
            offset *= 10;
        }
        // 首尾判断
        while (num != 0) {
            if (num / offset != num % 10) {
                return false;
            }
            num = (num % offset) / 10;
            offset /= 100;
        }
        return true;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值