2023年3月

Uniapp中的easycom是一种组件自动注册机制,可以让开发者更加方便地使用和管理组件。下面详细介绍下关于easycom使用方法。

什么是easycom

easycom是Uniapp框架提供的一种组件自动注册机制,它可以自动扫描指定目录下的所有组件,并注册到全局组件中。这意味着我们无需手动在components中引入组件,也无需在每个页面中单独引入组件,只需要在组件的目录下创建一个index.vue文件,就可以自动注册组件并在全局中使用了。

如何使用easycom?

使用easycom非常简单,只需要在项目根目录下的pages.json中配置easycom属性即可。例如:

{"easycom": {"autoscan": true,"custom": {"^cu-": "@/components/cu/"}
}
}

其中,
autoscan
表示是否启用自动扫描功能,如果设置为
true
,则会自动扫描项目中所有符合规则的组件并注册到全局中。如果设置为
false
,则需要手动在
components
中引入组件。

custom
是自定义规则,可以根据规则自动注册组件。例如上面的例子中,以
cu-
开头的组件会被自动注册到
@/components/cu/
目录下。

除了在
pages.json
中配置
easycom
属性外,还可以在单个页面的
json
文件中配置
usingComponents
属性来引用组件。例如:

{"usingComponents": {"cu-btn": "@/components/cu-btn/index"}
}

上面的例子中,
cu-btn
组件会被自动引入到当前页面中,无需手动在
components
中引入。

easycom的规则

easycom
支持多种规则,可以自定义组件的目录和组件名。以下是常见的规则:

  • 目录规则:将组件放在
    components
    目录下,文件名为
    index.vue
    ,则组件会自动注册到全局中。例如:
    components/my-component/index.vue
    会被自动注册为
    my-component
    组件。

  • 前缀规则:将组件放在任意目录下,文件名为
    index.vue
    ,文件名以
    指定前缀
    开头,例如
    my-
    ,则组件会自动注册到全局中。例如:
    components/my-component/index.vue
    会被自动注册为
    my-component
    组件。

  • 全路径规则:将组件放在任意目录下,文件名为
    index.vue
    ,则可以在页面中使用全路径来引用组件,例如:
    @/components/my-component/index

easycom的注意事项

虽然
easycom
提供了方便的组件自动注册机制,但

在使用
easycom
时,也有一些需要注意的事项:

  1. 组件命名必须是小写字母,使用短横线连接单词。例如:
    my-component

  2. 不同平台的组件可能有不同的实现方式,因此需要在
    pages.json
    中配置
    easycom
    属性时,需要根据平台分别配置。例如:


    {"easycom": {"nvue": {"autoscan": true},"h5": {"autoscan": true}
    }
    }

  3. 如果有一些组件不需要自动注册,可以在组件目录下创建一个
    .easycomignore
    文件来忽略该组件的自动注册。例如:


    # 忽略my-component组件
    my
    -component/

    如果需要忽略某个目录下的所有组件,可以在
    .easycomignore
    文件中输入目录名即可。

  4. 如果需要自定义规则,可以在
    pages.json
    中配置
    custom
    属性。例如:
    {"easycom": {"autoscan": true,"custom": {"^my-": "@/components/my/"}
    }
    }

    上面的例子中,以
    my-
    开头的组件会被自动注册到
    @/components/my/
    目录下。

  5. 如果需要在某个页面中引用组件,可以在页面的
    json
    文件中配置
    usingComponents
    属性。例如:
    {"usingComponents": {"my-component": "@/components/my-component/index"}
    }

上面的例子中,
my-component
组件会被自动引入到当前页面中。

总的来说,
easycom
是Uniapp框架中非常方便的组件自动注册机制,可以大大简化组件的使用和管理。但是在使用时需要注意一些规则和注意事项,以保证组件能够正常注册和使用。

OSS前端直传+后端签名

一、服务端签名后前端直传

首先安装阿里云SDK Aliyun.OSS.SDK.NetCore

        public static string accessKeyId = "你的accessKeyId";
        public static string accessKeySecret = "你的accessKeySecret";
        public static string bucketName = "你的桶名称";
        public static string endpoint = "oss-cn-beijing.aliyuncs.com";
        public static int expireTime = 30;
        public Dictionary<string, string> GetPolicy(string fileName)
        {
            var dir = DateTime.Now.ToString("yyyyMMdd") + "/";
            // 构造OssClient实例。 endpoint 格式:https://oss-cn-beijing.aliyuncs.com
            var ossClient = new OssClient("https://" + endpoint, accessKeyId, accessKeySecret);
            var config = new PolicyConditions();
            config.AddConditionItem(PolicyConditions.CondContentLengthRange, 1, 1024L * 1024 * 1024 * 5);// 文件大小范围:单位byte
            config.AddConditionItem(MatchMode.StartWith, PolicyConditions.CondKey, dir);
            var expire = DateTimeOffset.Now.AddMinutes(30);// 过期时间
            // 生成 Policy,并进行 Base64 编码
            var policy = ossClient.GeneratePostPolicy(expire.LocalDateTime, config);
            var policyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(policy));

            // 计算签名
            var hmac = new HMACSHA1(Encoding.UTF8.GetBytes(accessKeySecret));
            var bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(policyBase64));
            var sign = Convert.ToBase64String(bytes);
            // 将签名和回调的内容,返回给前端
            var host = $"https://{bucketName}.{endpoint}";
            var key = $"{dir}{Guid.NewGuid()}/{fileName}";
            var fullUrl = $"https://{bucketName}.{endpoint}/{key}";
            var rt = new Dictionary<string, string>
            {
                { "OSSAccessKeyId",accessKeyId},
                { "Host",host },
                { "key",key},
                { "policy",policyBase64},
                { "Signature",sign},
                { "success_action_status","200"},
                { "fullUrl",fullUrl },
                {"expire",expire.ToString() }
            };

            return rt;
        }

前端首先访问后端获取签名,获取签名后使用FromData的形式上传文件

async startUpload() {
      // 获取后端签名和上传地址
      const res = await axios.get("http://localhost:5152/api/OSS/GetPolicy", {
        params: {
          name: this.file.name
        }
      });
      var formData = new FormData();
      formData.append("name", this.file.name);
      formData.append("OSSAccessKeyId", res.data.OSSAccessKeyId);
      formData.append("key", res.data.key);
      formData.append("policy", res.data.policy);
      formData.append("signature", res.data.Signature);
      formData.append("success_action_status", res.data.success_action_status);
      formData.append("file", this.file);
      axios
        .post(res.data.Host, formData, {
          headers: {
            "Content-Type": "multipart/form-data"
          },
          withCredentials: false
        })
        .then(res => {
          console.log(res);
        });
    }

二、服务端STS签名前端分片上传+断点续传

当文件过大时,考虑使用分片上传和断点续传的方式来上传文件到oss,这时我们就不能直接使用accesskeyId和accessKeySecret的方式来在前端上传,以免暴露我们的密钥,当然也不能直接使用第一种的方式进行签名(或许可以,没有找到示例,也没有研究出来),所以我们采用STStoken的方式签名,然后在前端使用阿里云提供的SDK进行文件上传。

断点续传的思路是在每个分片上传的时候存储当前文件的上传进度,如果中间因为各种原因无法继续上传时,当用户重新上传同一个文件的时候,获取文件的上传进度,继续上传没有上传完的部分,而不是重新上传整个文件。为了确保断点续传前后上传的是同一个文件,我们使用md5作为存储进度的key值,如果是同一个文件,则续传,如果不是同一个文件,则从0开始上传。

首先登录阿里云开通sts账户和权限。

安装 aliyun-net-sdk-core和aliyun-net-sdk-sts sdk

public Dictionary<string, string> GetSTSToken()
        {
            //此处使用sts账户的id和secret
            var AccessKeyID = "***";
            var AccessKeySecret = "***";
            string bucketName = "***";
            // ststoken
            IClientProfile profile = DefaultProfile.GetProfile("oss-cn-beijing", AccessKeyID, AccessKeySecret);
            DefaultAcsClient client = new DefaultAcsClient(profile);
            var request = new AssumeRoleRequest();
            request.RoleArn = "***";
            request.RoleSessionName = "xxx";//这里的名字随便写
            request.DurationSeconds = 3600;//过期时间
            var response = client.GetAcsResponse(request);

            var result = new Dictionary<string, string>
            {
                {"AccessKeyId", response.Credentials.AccessKeyId},
                {"AccessKeySecret",response.Credentials.AccessKeySecret },
                {"SecurityToken",response.Credentials.SecurityToken },
                {"Expiration",response.Credentials.Expiration },
                {"BucketName",bucketName }
            };

            return result;
        }

签名完成后,安装阿里云oss sdk

npm install ali-oss;
npm install spark-md5;
<template>
  <div class="hello">
    <div>
      <input type="file" @change="fileChange" />
      <div>{{ progress }}</div>
    </div>
  </div>
</template>
#自行导入包,自行定义变量
async fileChange(e) {
      this.file = e.target.files[0];
      this.uploadFile(this.file);
    },
async uploadFile(file) {
      const objectKey = "xxx" + "/file/" + this.file.name;
      // 初始化 OSS 客户端 SDK
      await this.initOSSClient();
      this.resumeUpload(objectKey, file);
    }
# 首先初始化oss 对象
    async initOSSClient() {
      const res = await axios.get("http://localhost:5152/api/OSS/GetSTSToken");
      console.log(res);
      const {
        AccessKeyId,
        AccessKeySecret,
        SecurityToken,
        BucketName
      } = res.data;
      this.bucketName = BucketName;

      this.client = new OSS({
        region: "oss-cn-beijing",
        accessKeyId: AccessKeyId,
        accessKeySecret: AccessKeySecret,
        stsToken: SecurityToken,
        bucket: BucketName
      });
    },
    # 断点上传
    async resumeUpload(objectKey, file) {
      //使用SparkMd5计算文件的md5值
      let md5 =await this.calculateFileMD5(file);

      let checkpoint = JSON.parse(
        window.localStorage.getItem("checkpoint_" + md5)
      );
      var _this = this;
      // 重试五次。
      for (let i = 0; i < 5; i++) {
        try {
          const result = await this.client.multipartUpload(objectKey, file, {
            checkpoint,
            async progress(percentage, cpt) {
              checkpoint = cpt;
              _this.progress = parseInt(percentage * 100);
              // 将 checkpoint 保存到浏览器localstorage 中。
              window.localStorage.setItem(
                "checkpoint_" + md5,
                JSON.stringify(checkpoint)
              );
            }
          });
          // 删除本地保存的 checkpoint,如果此处不删除的话,上传成功后,用户无法再次上传同名文件
          window.localStorage.removeItem("checkpoint_" + md5);

          break; // 跳出当前循环。
        } catch (e) {
          console.log(e);
        }
      }
    },
    // 使用sparkMD5 计算文件md5
     calculateFileMD5(file, chunkSize = 2097152) {
      // chunkSize为分块大小,默认为2MB
      return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        let currentPosition = 0;
        const spark = new SparkMD5.ArrayBuffer();

        fileReader.onerror = function() {
          reject("文件读取失败!");
        };

        fileReader.onload = function() {
          spark.append(fileReader.result); // 将读取到的数据添加到MD5计算器中

          currentPosition += chunkSize;
          if (currentPosition < file.size) {
            // 文件还没读完,继续读取下一块
            loadNext();
          } else {
            // 文件读取完毕,计算MD5值并返回结果
            const hash = spark.end();
            resolve(hash);
          }
        };

        function loadNext() {
          const blob = file.slice(currentPosition, currentPosition + chunkSize);
          fileReader.readAsArrayBuffer(blob);
        }

        // 开始读取第一块
        loadNext();
      });
    }

如果想自己控制上传的各步骤可以使用initiateMultipartUpload uploadPart completeMultipartUpload 等方法自行实现各步骤,大致思路就是先initiateMultipartUpload初始化一个分片上传,返回uploadid,然后将文件按一定的大小分片,之后循环上传每个分片,完成分片之后调用completeMultipartUpload方法合并文件,这种方式比较复杂。

二维码识别技术已广泛应用在移动支付、实用工具、电商购物、社交通讯等场景。然而,在实际生活中,二维码容易遇到距离远、暗光、强光、污损、模糊和大角度倾斜等复杂场景,导致识别困难,扫码体验差。华为HMS Core
统一扫码服务
(Scan Kit)为开发者们的APP带来一站式扫码解决方案,并且拥有高识别率和快速识别等特点。

距离太远、码图过小?

在停车场扫码缴费、上课扫码签到、广告牌宣传等实际生活场景中,二维码的尺寸不一,一般的扫码功能需手动调节手机相机框与二维码的距离直到合适的扫码大小,实属不便。
统一扫码服务
基于自研的深度学习算法模型,遇到远距离场景、码图过小、甚至肉眼无法分辨的情况,支持自动检测放大,智能识别。

多角度、光线不佳、模糊等复杂场景无法识别?

餐桌、共享单车、充电桩等设备上的二维码,易受环境或人为影响,导致污损、模糊、反光,无法清晰展示,造成识别困难。
统一扫码服务
基于多项计算机视觉技术,可以大幅提升复杂场景识别率,统一扫码服务可以自动检测及旋转纠正,能够在识别任意角度的同时保证准确率。

单个扫码效率低?

在录入商品信息、快递信息等需要频繁扫码的场景中,一般的扫码功能识别成功后会自动跳转,操作不便。统一扫码服务为您带来连续扫码功能,对相机内连续出现的二维码识别检测,避免识别成功自动跳转的问题。同时,多码识别模式下,统一扫码服务最多可同时识别不限种类的5个码,大幅提升工作效率。

另外,HMS Core
统一扫码服务
为助力开发者们构建流畅的扫码体验,带来更加完善的功能:

① 支持识别/生成13种主流码格式;

② 支持自定义扫码界面;

③ 支持分析12个场景的码内容,提取结构化数据;

④ 提供内存 1.1M / 3.3M 两种SDK及4种不同类型的调用方式,安卓版本最少5行代码即可快速接入。

下面是接入 HMS Core 统一扫码服务的开发步骤,以Default View Mode为例,只需简单的集成就可为应用构建扫码能力。

开发步骤

1. 开发准备

详细准备步骤可参考
华为开发者联盟官网

2. 集成准备

Default View Mode提供相机扫码和导入图片扫码两个功能,提供完整的Activity,不需要您开发扫码界面。

注意:使用此种模式请确保您的应用没有关闭硬件加速,即“hardwareAccelerated”为“true”。否则在个别机型可能会出现黑屏问题。

3. 业务流程

使用Default View Mode的主要业务流程如下:

  1. 用户打开App发起扫码申请。

  2. 校验是否有相机权限。

  3. 权限验证通过后,调用HMS Core SDK的
    startScan
    扫码接口,启动扫码界面。若需要满足最小权限和场景化触发要求,文件读取权限在用户点击后触发场景,需要设置错误监听,检测到无文件读取权限,Scan Kit报错退出Default View。

  4. 判断扫码页面启动状态。

  5. HMS Core SDK回调应用的“onActivityResult”接口。

  6. 根据扫码状态
    RESULT_CODE
    获取扫码结果:若
    SUCCESS
    返回结果给用户;若结果为
    ERROR_NO_READ_PERMISSION
    ,需要应用主动申请文件读取权限,重新进入Default View。

  7. App封装扫码结果返回给用户。

  1. 开发步骤
  1. (可选)根据实际需求创建扫码选项参数。

Scan Kit默认支持
13种码制式
,您也可以指定Scan Kit只扫描特定的码制式以提高扫码速度。例如,当仅需要检测QR码和DataMatrix码时,请按照以下示例构建
HmsScanAnalyzerOptions
对象。如果不限制检测码格式,您可以不创建
HmsScanAnalyzerOptions
对象。“1”为您设置的扫码标题样式参数,对应
setViewType
方法中的“var1”参数。

// “QRCODE_SCAN_TYPE”和“DATAMATRIX_SCAN_TYPE”表示只扫描QR和DataMatrix的码,setViewType设置扫码标题,0表示设置扫码标题为”扫描二维码/条码“,1表示设置扫码标题为”扫描二维码“,默认为0;setErrorCheck设置错误监听,true表示监听错误并退出扫码页面,false表示不上报错误,仅检查到识别结果后退出扫码页面,默认为false

HmsScanAnalyzerOptions options = new HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.QRCODE_SCAN_TYPE,HmsScan.DATAMATRIX_SCAN_TYPE).setViewType(1).setErrorCheck(true).create();
  1. 调用
    ScanUtil
    的静态方法
    startScan
    启动Default View扫码页面。用户可以使用相机扫码,也可以通过该页面的“导入图片”按钮检测图片中的码。

“REQUEST_CODE_SCAN_ONE”为您设置的请求码参数,对应“onActivityResult”方法中的“requestCode”参数,用于判断“onActivityResult”调用是否来自Scan Kit扫码结果回调。若“requestCode”参数等于您设置的请求码参数,则表示本次“onActivityResult”是Scan Kit的扫码结果回调。

如果您没有指定只检测特定的码制式,此处的options可以置为“null”,表示默认检测
Scan Kit支持的码制式

ScanUtil.startScan(this, REQUEST_CODE_SCAN_ONE, options);
  1. 实现回调接口接收扫码结果,相机扫码和导入图片扫码均通过该接口返回。

    调用Activity的“onActivityResult”方法获取Intent参数,扫描结果对象HmsScan封装在其中。如何获取Intent参数请参见
    RESULT

若“requestCode”等于
步骤2
中定义的“REQUEST_CODE_SCAN_ONE”请求码参数,则表示本次接收的Intent是Scan Kit返回的结果。

通过Intent中的
RESULT_CODE
获取扫码状态。

通过Intent中的
RESULT
获取扫码结果
HmsScan
,其中包含的信息参见
码值解析

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode != RESULT_OK || data == null) {
        return;
    }
    if (requestCode == REQUEST_CODE_SCAN_ONE) {
        // 导入图片扫描返回结果
        int errorCode = data.getIntExtra(ScanUtil.RESULT_CODE, ScanUtil.SUCCESS);
        if (errorCode == ScanUtil.SUCCESS) {
        Object obj = data.getParcelableExtra(ScanUtil.RESULT);
        if (obj != null) {
                // 展示扫码结果
        ...
            }
    }
        if (errorCode == ScanUtil.ERROR_NO_READ_PERMISSION) {
            // 无文件权限,请求文件权限
        ...
        }
    }
}

了解更多详情>>

访问
华为开发者联盟官网
获取
开发指导文档
华为移动服务开源仓库地址:
GitHub

Gitee

关注我们,第一时间了解 HMS Core 最新技术资讯~

Python
是一种高级编程语言,它易于学习和使用,因此成为了许多人的首选编程语言。本文将介绍
Python
的基础知识,以帮助初学者快速入门。

安装Python

在开始学习Python之前,您需要安装Python。您可以从Python官方网站下载Python的安装程序。安装程序将指导您完成安装过程。

Python基础知识

变量

在Python中,变量用于存储值。您可以使用等号(=)来赋值。例如:

x = 5
y = "Hello, world!"

数据类型

Python支持许多数据类型,包括数字、字符串和布尔值。以下是一些常见的数据类型:

  • int(整数)
    :例如1、2、3等。
  • float(浮点数)
    :例如1.0、2.5、3.14等。
  • str(字符串)
    :例如"Hello, world!"、"Python入门指南"等。
  • bool(布尔值)
    :True或False。

运算符

Python支持许多运算符,包括算术运算符、比较运算符和逻辑运算符。以下是一些常见的运算符:

  • 算术运算符
    :+、-、*、/等。
  • 比较运算符
    :==、!=、>、<等。
  • 逻辑运算符
    :and、or、not等。

控制流

Python支持许多控制流语句,包括if语句、for循环和while循环。以下是一些常见的控制流语句:

  • if语句用于根据条件执行不同的代码块。以下是一个使用if语句的示例:
x = 5
if x > 0:
    print("x is positive")
elif x == 0:
    print("x is zero")
else:
    print("x is negative")

在上面的示例中,如果x大于0,则打印
x is positive
。如果x等于0,则打印
x is zero
。否则,打印
x is negative

  • while循环用于重复执行代码块,直到指定的条件不再满足为止。以下是一个使用while循环的示例:
i = 1
while i <= 5:
    print(i)
    i += 1

在上面的示例中,代码块将重复执行,直到i的值大于5为止。每次循环时,将打印
i
的值,并将i的值增加1。

  • for循环用于遍历序列中的元素。以下是一个使用for循环的示例:
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

在上面的示例中,代码块将遍历
fruits
列表中的元素,并将每个元素打印到控制台上。

接下来,我们将介绍
Python
的函数和模块。

函数


Python
中,函数用于执行特定的任务。您可以使用
def
关键字定义函数。例如:

def greet(name):
    print("Hello, " + name + "!")

模块

Python
中的模块是一个包含
Python
代码的文件。您可以使用
import
语句导入模块。例如:

import math

print(math.pi)

结论

本文介绍了
Python
的基础知识,包括变量、数据类型、运算符、控制流、函数和模块。希望这篇
Python
入门指南能够帮助您快速入门
Python
编程。

本文已收录到
AndroidFamily
,技术和职场问题,请关注公众号 [彭旭锐] 提问。

大家好,我是小彭。

上周末是 LeetCode 第 337 场周赛,你参加了吗?这场周赛第三题有点放水,如果按照题目的数据量来说最多算 Easy 题,但如果按照动态规划来做可以算 Hard 题。


小彭的技术交流群 02 群来了,公众号回复 “加群” 加入我们~


周赛概览

2595.  奇偶位数(Easy)

  • 题解一:模拟 $O(lgn)$
  • 题解二:位掩码 + bitCount $O(1)$

2596.  检查骑士巡视方案(Medium)

  • 题解一:模拟 $O(n^2)$

2597.  美丽子集的数目(Medium)

  • 题解一:回溯 $O(2^n)$
  • 题解二:同余分组 + 动态规划 / 打家劫舍 $O(nlgn)$

2598.  执行操作后的最大 MEX(Medium)

  • 题解一:同余分组 + 贪心 $O(n)$


2595.  奇偶位数(Easy)

题目地址

https://leetcode.cn/problems/number-of-even-and-odd-bits/

题目描述

给你一个

整数
n


even
表示在
n
的二进制形式(下标从
0
开始)中值为
1
的偶数下标的个数。


odd
表示在
n
的二进制形式(下标从
0
开始)中值为
1
的奇数下标的个数。

返回整数数组
answer
,其中
answer = [even, odd]

题解一(模拟)

简单模拟题。

写法 1:枚举二进制位

class Solution {
    fun evenOddBit(n: Int): IntArray {
        val ret = intArrayOf(0, 0)
        for (index in 0..30) {
            if (n and (1 shl index) != 0) {
                ret[index % 2]++
            }
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:$O(U)$ 其中 $U$ 是整数二进制位长度;
  • 空间复杂度:$O(1)$ 仅使用常量级别空间。

写法 2:不断取最低位,然后右移 n,当 n 为 0 时跳出:

class Solution {
    fun evenOddBit(n: Int): IntArray {
        val ret = intArrayOf(0, 0)
        var x = n
        var index = 0
        while (x != 0) {
            ret[i] += x and 1 // 计数
            x = x ushr 1 // 右移
            i = i xor 1 // 0 -> 1 或 1 -> 0
        }
        return ret
    }
}

复杂度分析:

  • 时间复杂度:$O(lgn)$
  • 空间复杂度:$O(1)$ 仅使用常量级别空间。

题解二(位掩码 + bitCount)

使用二进制掩码 01010101 取出偶数下标,再使用 Integer.bitCount() 计算位 1 的个数:

class Solution {
    fun evenOddBit(n: Int): IntArray {
        val mask = 0b0101_0101_0101_0101_0101_0101_0101_0101
        return intArrayOf(
            Integer.bitCount(n and mask),
            Integer.bitCount(n) - Integer.bitCount(n and mask)
        )
    }
}

复杂度分析:

  • 时间复杂度:$O(1)$ Java Integer.bitCount() 库函数的时间复杂度是 $O(1)$,如果按照常规实现是 $O(lgn)$;
  • 空间复杂度:$O(1)$


2596.  检查骑士巡视方案(Medium)

题目地址

https://leetcode.cn/problems/check-knight-tour-configuration/

题目描述

骑士在一张
n x n
的棋盘上巡视。在有效的巡视方案中,骑士会从棋盘的
左上角
出发,并且访问棋盘上的每个格子
恰好一次

给你一个
n x n
的整数矩阵
grid
,由范围
[0, n * n - 1]
内的不同整数组成,其中
grid[row][col]
表示单元格
(row, col)
是骑士访问的第
grid[row][col]
个单元格。骑士的行动是从下标
0
开始的。

如果
grid
表示了骑士的有效巡视方案,返回
true
;否则返回
false

注意
,骑士行动时可以垂直移动两个格子且水平移动一个格子,或水平移动两个格子且垂直移动一个格子。下图展示了骑士从某个格子出发可能的八种行动路线。

题解(模拟)

二维简单模拟题。

class Solution {
    fun checkValidGrid(grid: Array<IntArray>): Boolean {
        if (grid[0][0] != 0) return false
        val directions = arrayOf(
            intArrayOf(1, 2),
            intArrayOf(2, 1),
            intArrayOf(2, -1),
            intArrayOf(1, -2),
            intArrayOf(-1, -2),
            intArrayOf(-2, -1),
            intArrayOf(-2, 1),
            intArrayOf(-1, 2)
        )
        val n = grid.size
        var row = 0
        var column = 0
        var count = 1
        outer@ while (count < n * n) {
            for (direction in directions) {
                val newRow = row + direction[0]
                val newColumn = column + direction[1]
                if (newRow < 0 || newRow >= n || newColumn < 0 || newColumn >= n) continue
                if (count == grid[newRow][newColumn]) {
                    row = newRow
                    column = newColumn
                    count++
                    continue@outer
                }
            }
            return false
        }
        return true
    }
}

复杂度分析:

  • 时间复杂度:$O(C·n^2)$ 其中 $C$ 是骑士的行走方向,$C$ 为常数 8;
  • 空间复杂度:$O(1)$


2597.  美丽子集的数目(Medium)

题目地址

https://leetcode.cn/problems/the-number-of-beautiful-subsets/

题目描述

给你一个由正整数组成的数组
nums
和一个

整数
k

如果
nums
的子集中,任意两个整数的绝对差均不等于
k
,则认为该子数组是一个
美丽
子集。

返回数组
nums

非空

美丽
的子集数目。

nums
的子集定义为:可以经由
nums
删除某些元素(也可能不删除)得到的一个数组。只有在删除元素时选择的索引不同的情况下,两个子集才会被视作是不同的子集。

预备知识

  • 同余性质:

如果
x % m == y % m
,则称 x 和 y 对模 m 同余,即为
x ≡ (y mod m)

  • 乘法定理:

如果某个任务有 n 个环节,每个环节分别有 ${m_1, m_2, m_3, …, m_n}$ 种方案,那么完成任务的总方案数就是 $m_1
m_2
m3

m_n$。

题解一(暴力回溯)

由于题目的数据量非常小(数组长度只有 20),所以可以直接使用暴力算法。

算法:枚举所有子集,并且增加约束条件:如果已经选择过
x - k

x + k
,则不能选择
x

class Solution {
    private var ret = 0

    fun beautifulSubsets(nums: IntArray, k: Int): Int {
        subsets(nums, 0, k, IntArray(k + 1001 + k)/* 左右增加 k 避免数组下标越界 */)
        return ret - 1 // 题目排除空集
    }

    // 枚举子集
    private fun subsets(nums: IntArray, start: Int, k: Int, cnts: IntArray) {
        // 记录子集数
        ret++
        if (start == nums.size) return

        for (index in start until nums.size) {
            val x = nums[index] + k // 偏移 k
            if (cnts[x - k] != 0 || cnts[x + k] != 0) continue // 不允许选择
            // 选择
            cnts[x]++
            // 递归
            subsets(nums, index + 1, k, cnts)
            // 回溯
            cnts[x]--
        }
    }
}

复杂度分析:

  • 时间复杂度:$O(2^n)$ 其中 $n$ 为 $nums$ 数组长度,每个位置有选和不选两种状态,每个状态的时间复杂度是 $O(1)$,因此整体时间复杂度是 $O(2^n)$;
  • 空间复杂度:$O(C)$ 数组空间。

题解二(同余分组 + 动态规划 / 打家劫舍)

这道题如果不使用暴力解法,难度应该算 Hard。

根据同余性质,如果 x 和 y 对模 k 同余,那么 x 和 y 有可能相差 k;如果 x 和 y 对模 k 不同余,那么 x 和 y 不可能相差 k。
因此,我们可以将原数组按照模 k 分组,然后单独计算每个分组内部方案数,再根据乘法定理将不同分组的方案数相乘即得到最终结果。

那么,现在的是如何计算分组内部的方案数?

我们发现,
“如果已经选择过
x - k

x + k
,则不能选择
x

的语义跟经典动态规划题
198.打家劫舍

“如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警”
的语义非常类似,我们可以套用相同的解题思路:

1、先对分组内部排序,因为只有相邻的数才有可能不能同时选择;

2、定义 dp[i] 表示选择到 i 为止的方案数,则有递推关系:

$$
dp[i] = \begin{cases}
dp[i-1] + dp[i-2] &\text{if } a_i - a_{i-1} =k\
dp[i-1]*2 &\text{if } a_i - a_{i-1} \not=k
\end{cases}
$$

如果不选 $a_i$,那么 $a_{i-1}$ 一定可选,即 $dp[i-1]$ 的方案数。

如果选择 $a_i$,那么需要考虑 $a_{i-1}$ 与 $a_i$ 的关系:

  • 如果 $a_i - a_{i-1} =k$,那么 $a_i$ 与 $a_{i-1}$ 不能同时选择,$dp[i] = dp[i-1] + dp[i-2]$ 表示在 $a_{i-1}$ 和 $a_{i-2}$ 上的方案数之和;
  • 如果 $a_i - a_{i-1} \not=k$,那么 $a_i$ 与 $a_{i-1}$ 可以同时选择 $dp[i] = dp[i-1]*2$

最后,还需要考虑数字重复的情况,设 c_i 表示 a_i 的出现次数:

  • 如果不选 $a_i$,则只有 1 种方案,即空集;
  • 如果选择 $a_i$,则有 $2^{c_i}$ 种方案,最后在减去 1 个空集方案。

整理到递归公式中有:

$$
dp[i] = \begin{cases}
dp[i-1] + dp[i-2]
(2^{c_i}-1) &\text{if } a_i - a_{i-1} =k\
dp[i-1]

(2^{c_i}) &\text{if } a_i - a_{i-1} \not=k
\end{cases}
$$

以 [2, 3, 4, 5, 10], k = 2 为例,按照模 2 分组后:

  • 余数为 0 的分组 [2, 4, 10]:方案为 [2]、[4]、[10]、[2, 10]、[4, 10]
  • 余数为 1 的分组 [3, 5]:方案为 [3]、[5]
class Solution {
    fun beautifulSubsets(nums: IntArray, k: Int): Int {
        // 1、同余分组
        // <余数 to <元素,元素计数>>
        val buckets = HashMap<Int, TreeMap<Int, Int>>()
        for (num in nums) {
            val cntMap = buckets.getOrPut(num % k) { TreeMap<Int, Int>() }
            cntMap[num] = cntMap.getOrDefault(num, 0) + 1
        }
        // 2、枚举分组
        var ret = 1
        for ((_, bucket) in buckets) {
            // 3、动态规划
            val n = bucket.size
            // dp[i] 表示选择到 i 位置的方案数,这里使用滚动数组写法
            // val dp = IntArray(n + 1)
            var pre2 = 0 // dp[i-2]
            var pre1 = 1 // dp[i-1]
            var index = 1
            var preNum = -1
            for ((num, cnt) in bucket) {
                if (index > 1 && num - preNum == k) {
                    // a_i 不可选
                    val temp = pre1
                    pre1 = pre1 + pre2 * ((1 shl cnt) - 1)
                    pre2 = temp
                } else {
                    // a_i 可选可不选
                    val temp = pre1
                    pre1 = pre1 * (1 shl cnt)
                    pre2 = temp
                }
                preNum = num
                index++
            }
            ret *= pre1
        }
        return ret - 1
    }
}

复杂度分析:

  • 时间复杂度:$O(nlgn + n)$ 其中 $n$ 为 $nums$ 数组的长度,使用有序集合进行分组的时间复杂度是 $O(nlgn)$,枚举分组的步骤每个元素最多访问一次,时间复杂度是 $O(n)$;
  • 空间复杂度 $O(n)$:有序集合空间 $O(n)$,动态规划过程中使用滚动数组空间为 $O(1)$。

相似题目

近期周赛子集问题:

LeetCode 周赛 333 ·  无平方子集计数(Medium)


2598.  执行操作后的最大 MEX(Medium)

题目地址

https://leetcode.cn/problems/smallest-missing-non-negative-integer-after-operations/

题目描述

给你一个下标从
0
开始的整数数组
nums
和一个整数
value

在一步操作中,你可以对
nums
中的任一元素加上或减去
value

  • 例如,如果
    nums = [1,2,3]

    value = 2
    ,你可以选择
    nums[0]
    减去
    value
    ,得到
    nums = [-1,2,3]

数组的 MEX (minimum excluded) 是指其中数组中缺失的最小非负整数。

  • 例如,
    [-1,2,3]
    的 MEX 是
    0
    ,而
    [1,0,3]
    的 MEX 是
    2

返回在执行上述操作
任意次
后,
nums
的最大 MEX

预备知识

  • 同余性质:

如果
x % m == y % m
,则称 x 和 y 对模 m 同余,即为
x ≡ (y mod m)

  • 负数取模技巧:

如果 x 和 y 都大于 0,则
x ≡ (y mod m)
等价于
x % m == y % m

$$
\begin{matrix}
10\ % \ 3 == 1\
-4\ %\ 3 == 1
\end{matrix}
$$

如果 x 和 y 都小于 0,则
x ≡ (y mod m)
等价于
x % m == y % m

$$
\begin{matrix}
-10\ %\ 3== -1\
-4\ %\ 3==-1
\end{matrix}
$$

如果 x < 0,而 y≥0,则
x ≡ (y mod m)
等价于
x % m + m == y % m

$$
\begin{matrix}
-10\ %\ 3== -1 + 3 == 2\
-4\ %\ 3 == -1 + 3 == 2\
5\ %\ 3==2
\end{matrix}
$$

可以看到,为了避免考虑正负数,可以统一使用
(x % m + m) % m

x
取模,如此无论
x
为正负数,余数都能转换到
[0,m)
范围内。

题解(同余分组 + 贪心)

这道题依然考同余性质。

根据同余性质,如果 x 和 y 对模 value 同余,那么经过若干次操作后 x 总能转换为 y。如果 x 和 y 对模 value 不同余,那么无论经过多少次操作 x 也无法转换为 y。

举个例子:{-10、-4、-1、2、5} 都对模 3 同余,而 1 对模 3 不同余。只要经过若干次 +value/-value,总能转换为同余的其他数;

  • -10 + 3 * 2 = -4
  • -10 + 3 * 3 = -1
  • -10 + 3 * 4 = 2
  • -10 + 3 * 5 = 5
  • 其它同理。
  • -10 无法转换为 1

贪心思路:继续分析题目,由于不同分组中的数不可能转换为其它分组的数,为了让目标
“确实的最小非负整数尽可能大”
,应该取尽可能小的同余数,否则无法使结果更优。

举个例子,假设
value
为 3,且不同分组的个数如下:

  • 余数为 0 的分组中有 3 个元素:应该取 0、3、6
  • 余数为 1 的分组中有 4 个元素:应该取 1、4、7、10
  • 余数为 2 的分组中有 1 个元素:应该取 2、[缺失 5]

如果余数为 0 的分组取 0、6、9,则缺失的元素会变成 4。

class Solution {
    fun findSmallestInteger(nums: IntArray, value: Int): Int {
        // 同余分组
        val buckets = HashMap<Int, Int>()
        for (num in nums) {
            val mod = (num % value + value) % value
            buckets[mod] = buckets.getOrDefault(mod, 0) + 1
        }
        // 试错
        var mex = 0
        while (true) {
            val mod = mex % value // mex 一定是非负数
            buckets[mod] = buckets.getOrDefault(mod, 0) - 1
            // 计数不足
            if (buckets[mod]!! < 0) break
            mex++
        }
        return mex
    }
}

复杂度分析:

  • 时间复杂度:$O(n)$ 其中 $n$ 为 $nums$ 数组的长度,计数时间为 $O(n)$,试错最多尝试 $n$ 次,整体时间复杂度为 $O(n)$;
  • 空间复杂度:$O(value)$ 散列表最多记录 $value$ 个分组。

相似题目:

OK,这场周赛就讲这么多,我们下周见。