力扣每日一题37:解数独

目录

题目

大致思路

方法一:回溯剪枝(正常人能想出来的)

方法二:位运算优化剪枝

需要使用的位运算技巧

代码

位运算怎么就优化了呢?

方法三:枚举优化。

官解代码

方法四:舞蹈链算法


题目

困难

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 1:

输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

提示:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] 是一位数字或者 '.'
  • 题目数据 保证 输入数独仅有一个解

面试中遇到过这道题?

1/5

通过次数

248.8K

提交次数

367.3K

通过率

67.7%

大致思路

使用一个数据记录每个每一行,每一列,每一个3*3宫格中数组1-9出现的情况。

对所有空格(也就是需要填数的地方)进行遍历填数,判断这个位置能填1-9中的哪个数字,如果数字num在空格所在的行、列、3*3宫格都没出现过,那就可以把num放进去,然后填写下一个空格。当填写完最后一个空格的时候,就代表解完了。

方法一:回溯剪枝(正常人能想出来的)

对于每一行、每一列、每个3*3宫格中1-9出现的情况,我们都用一个哈希表表示。

这样在对某个空格进行填数num时,我们只需要判断这个空格所在行、所在列、所在3*3宫格有没有出现过num即可。

class Solution {
public://判断九行、九列、九个格中数字1-9有无出现的哈希表int rows[9][9];int columns[9][9];int boxes[3][3][9];vector<pair<int,int>> blanks;//需要填写的空格bool flag;//用来剪枝void solveSudoku(vector<vector<char>>& board) {//初始化哈希表memset(rows,0,sizeof(rows));memset(columns,0,sizeof(columns));memset(boxes,0,sizeof(boxes));flag=false;//遍历一次9*9宫,寻找空格并更新哈希表int i,j;for(i=0;i<9;i++){for(j=0;j<9;j++){char c=board[i][j];if(c=='.'){blanks.emplace_back(i,j);//blanks.emplace_back({i,j});会有语法错误}else{int digit=c-'0';rows[i][digit-1]=columns[j][digit-1]=boxes[i/3][j/3][digit-1]=1;}}}backtrack(board,0);}void backtrack(vector<vector<char>>& board,int index){//填完了最后一个空格,if(index==blanks.size()){flag=true;return;}//1-9遍历int x=blanks[index].first;int y=blanks[index].second;for(int num=1;num<=9&&flag==false;num++){if(rows[x][num-1]==0&&columns[y][num-1]==0&&boxes[x/3][y/3][num-1]==0){rows[x][num-1]=columns[y][num-1]=boxes[x/3][y/3][num-1]=1;board[x][y]=num+'0';backtrack(board,index+1);rows[x][num-1]=columns[y][num-1]=boxes[x/3][y/3][num-1]=0;}}}
};

方法二:位运算优化剪枝

看了题解我才知道这也能优化…………

在方法一中,我们使用了长度为 9 的数组表示每个数字是否出现过(1为出现过,0为没有出现)。我们同样也可以借助位运算,仅使用一个整数表示每个数字是否出现过。

具体的做法是:一个二进制数的第i位数(最低为是第0位)是1,就表示 i+1 已经出现过。例如:

二进制数 (011010100) 代表数字 3、5、7、8已经出现过。

也就是说我们用一个二进制数来表示一个哈希表。

在方法一中,每在一个空格出现一个数字num,或者是填写一个数字num,都要将这个空格对于行、列、3*3宫格的哈希表的num值由0变成1,也就是 简单的[num-1]=1,填写完num发现数字不对后,又要将哈希表num的值由1变成0。也就是说,在用数组当作哈希表时,哈希表的更新就是将数组对应的值简单的0,1互换。

在此方法中,哈希表的更新就变成了二进制数对应某一位上的0,1互换。要使用一些位运算的技巧,

需要使用的位运算技巧

(1)、将二进制数的第i位由i变成0,或由0变成1.

1<<i 的第i+1位二进制数是1,其余的二进制数是0。x和1<<i按位异或后,x=x^(1<<i)后,x的第i+1位就实现了0,1的互换。

(2)、遍历一个二进制数上的所有0。

方法一中,空格所在的行、列、3*3宫格的哈希表[num-1]==0就代表这个空格可以填数字num,而在二进制的哈希表中,我们要找二进制数对应的0。二进制数的0不太好取,我们就把0,1互换,互换后的1就是先前的0,而二进制的1是好取的(随后会讲)。

一个二进制数按位取反得到的1就是先前的0,但是我们只需要前9位的1,所以取反后的数还要和 (111111111)进行按位与操作,这样就能只留下前九位数。

(3)、取得二进制数最低位的1(包括后面的0)

x=~x&(111111111) 后,x中第i(从最低为第0位开始)位的1就代表数字 i+1可以填在空格里。

low_one=x&(-x)。因为-x在计算器用的是补码,所以-x和x的二进制数的最低为1和右边的0都相等,其余为都不想等,按位与刚好可以留下这一部分。留下这一部分后,low_one的位数就是可以填入空格的数字。

(4)、将二进制数的最低为1变成0。

在某个空格blanks中填入某个数后不一定对,这时候要填下一个数。我们只需要将最低位的1变成0,然后再取最低位的1,这样就取到了下一个数。

x&=(x-1);

代码

class Solution {
private:int rows[9];int cols[9];int boxes[3][3];vector<pair<int, int>> blanks;bool valid;
public://将指定数位1变0,0变1void func(int i, int j, int digit) {rows[i] ^= (1 << digit);cols[j] ^= (1 << digit);boxes[i / 3][j / 3] ^= (1 << digit);}void backtrack(vector<vector<char>>& board, int index) {if (index == blanks.size()) {valid = true;return;}int x = blanks[index].first;int y = blanks[index].second;//创建掩码来遍历int mask = ~(rows[x] | cols[y] | boxes[x / 3][y / 3]);mask = mask & 0x1ff;            //和(111111111)按位取和,去除第九位之前的1while (mask && !valid) {//取最低位的1包括后面的0,然后转换成对应的数字int lowdigit = mask & (-mask);int digit = 0;while (lowdigit) {lowdigit >>= 1;digit++;}func(x, y, digit - 1);board[x][y] = '0' + digit;backtrack(board, index + 1);func(x, y, digit - 1);//去除掉最低位的1mask &= (mask - 1);}}void solveSudoku(vector<vector<char>>& board) {memset(rows, 0, sizeof(rows));memset(cols, 0, sizeof(cols));memset(boxes, 0, sizeof(boxes));valid = false;for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {if (board[i][j] == '.') {blanks.emplace_back(i, j);}else {int digit = board[i][j] - '0' - 1;func(i, j, digit);}}}backtrack(board, 0);}
};

位运算怎么就优化了呢?

对比一下方法一和方法二的代码。区别就是哈希表的更新和在遍历空格时,对于填充数字的遍历。


先说哈希表的更新,方法一的代码是

rows[i][digit-1]=columns[j][digit-1]=boxes[i/3][j/3][digit-1]=1;
或
rows[i][digit-1]=columns[j][digit-1]=boxes[i/3][j/3][digit-1]=0;

而方法二是对下面函数的一次调用。

void func(int i, int j, int digit) {rows[i] ^= (1 << digit);cols[j] ^= (1 << digit);boxes[i / 3][j / 3] ^= (1 << digit);}

光看这一部分来说,方法二反而要更复杂。

但是考虑到对空格填充数字的遍历时,那就不一样了。

方法一是对数字1-9都有一次判断,一共有九次。

for(int num=1;num<=9&&flag==false;num++){if(rows[x][num-1]==0&&columns[y][num-1]==0&&boxes[x/3][y/3][num-1]==0){//填充数字}
}

而方法二是在创建掩码后,每次只取对应二进制数位上有1的数。

//创建掩码来遍历int mask = ~(rows[x] | cols[y] | boxes[x / 3][y / 3]);mask = mask & 0x1ff;            //和(111111111)按位取和,去除第九位之前的1while (mask && !valid) {//填充数字mask &= (mask - 1);}

假设某个空格所在行、列、3*3宫格都只有一个数9可以填的时候,方法一需要循环9次,而方法二在创建掩码后,只需要循环1次。也就是说,方法二自动过滤掉了空格所在行、列、3*3宫格中已经出现的数。这大大缩短了代码运行时间。

方法三:枚举优化。

看了题解还知道,位运算的方法还能优化………………

想一下,我们在手作数独游戏的时候,都是先将能确定数字的位置先填上。

就比如在刚开始给出的矩阵中,有一个空格所在行出现过1、2、3,所在列出现过4、5、6,所在九宫格出现过7,8。那么毫无疑问,这个位置就是填9,不用等其他位置填完也能确认。

这样一来,我们可以不断地对整个数独进行遍历,将可以唯一确定的空白格全部填入对应的数。随后我们再使用与方法二相同的方法对剩余无法唯一确定的空白格进行递归 + 回溯。

官解代码

class Solution {
private:int line[9];int column[9];int block[3][3];bool valid;vector<pair<int, int>> spaces;public:void flip(int i, int j, int digit) {line[i] ^= (1 << digit);column[j] ^= (1 << digit);block[i / 3][j / 3] ^= (1 << digit);}void dfs(vector<vector<char>>& board, int pos) {if (pos == spaces.size()) {valid = true;return;}auto [i, j] = spaces[pos];int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;for (; mask && !valid; mask &= (mask - 1)) {int digitMask = mask & (-mask);int digit = __builtin_ctz(digitMask);flip(i, j, digit);board[i][j] = digit + '0' + 1;dfs(board, pos + 1);flip(i, j, digit);}}void solveSudoku(vector<vector<char>>& board) {memset(line, 0, sizeof(line));memset(column, 0, sizeof(column));memset(block, 0, sizeof(block));valid = false;for (int i = 0; i < 9; ++i) {for (int j = 0; j < 9; ++j) {if (board[i][j] != '.') {int digit = board[i][j] - '0' - 1;flip(i, j, digit);}}}while (true) {int modified = false;for (int i = 0; i < 9; ++i) {for (int j = 0; j < 9; ++j) {if (board[i][j] == '.') {int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;if (!(mask & (mask - 1))) {int digit = __builtin_ctz(mask);flip(i, j, digit);board[i][j] = digit + '0' + 1;modified = true;}}}}if (!modified) {break;}}for (int i = 0; i < 9; ++i) {for (int j = 0; j < 9; ++j) {if (board[i][j] == '.') {spaces.emplace_back(i, j);}}}dfs(board, 0);}
};

方法四:舞蹈链算法

我以为吕布已经天下无敌了,没想到有人比他还要勇猛…………

我翻看评论的时候,看到有个大佬发的一种算法,叫做舞蹈链算法,自称是解数独最快的算法,没有之一。这算法我也不会,直接把代码贴上来吧。

/*
最快的是舞蹈链算法
然后是贪心加回溯算法
最慢的是正常的回溯算法
*/
#include <bits/stdc++.h>using namespace std;const int N = 9 * 9 * 9 * 4;class node {
public:int u{}, d{}, l{}, r{}; //上下左右int row{}, col{};   //具体坐标int s{};          //col节点数量int h{};          //row头节点int ans{};        //选了那些rownode() = default;
};class DLX {
public:vector<vector<int>> a;const int n = 9 * 9 * 9, m = 9 * 9 * 4;int num{};vector<node> nodes;explicit DLX(vector<vector<int>> &B) {a = B;dance();B = a;}void dance() {init();for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {for (int k = 1; k <= 9; k++) {if (a[i][j] != k && a[i][j] != 0) {continue;}int id = i * 9 * 9 + j * 9 + k;Link(id, i * 9 + j + 1);Link(id, i * 9 + k + 81);Link(id, j * 9 + k + 162);Link(id, (i / 3 * 3 + j / 3) * 9 + k + 243);}}}Dance(0);}void init() {num = m + 1;nodes.assign(N, node());for (int i = 0; i <= m; i++) {node &it = nodes[i];it.l = i - 1;it.r = i + 1;it.u = it.d = i;}nodes[0].l = m;nodes[m].r = 0;}void Link(int r, int c) {num++;node &row = nodes[r];node &col = nodes[c];node &nums = nodes[num];nums.row = r;nums.col = c;col.s++;// node[col.u] <-> nums <-> colnums.u = col.u;nodes[col.u].d = num;col.u = num;nums.d = c;if (row.h == 0) {row.h = nums.l = nums.r = num;} else {node &head = nodes[row.h];//node[head.l] <-> nums <-> headnums.l = head.l;nodes[head.l].r = num;head.l = num;nums.r = row.h;}}void Remove(int c) {node &col = nodes[c];// node[col.l] <-> node[col.r]nodes[col.l].r = col.r;nodes[col.r].l = col.l;//下右for (int i = col.d; i != c; i = nodes[i].d) {for (int j = nodes[i].r; j != i; j = nodes[j].r) {node &it = nodes[j];//node[it.u] <-> node[it.d]nodes[it.d].u = it.u;nodes[it.u].d = it.d;nodes[it.col].s--;}}}void Resume(int c) {// node[col.l] -> node[c] <- node[col.r]node &col = nodes[c];nodes[col.r].l = c;nodes[col.l].r = c;//上左for (int i = nodes[c].u; i != c; i = nodes[i].u) {for (int j = nodes[i].l; j != i; j = nodes[j].l) {node &it = nodes[j];//node[it.u] -> node[j] <- node[it.d]nodes[it.d].u = j;nodes[it.u].d = j;nodes[it.col].s++;}}}int Dance(int dep) { // NOLINT(*-no-recursion)if (nodes[0].r == 0) {for (int i = 0; i < dep; i++) {node &it = nodes[i];int x = (it.ans - 1) / 9 / 9;int y = (it.ans - 1) / 9 % 9;int val = (it.ans - 1) % 9 + 1;a[x][y] = val;}return 1;}int c = nodes[0].r;for (int i = nodes[0].r; i != 0; i = nodes[i].r) {if (nodes[i].s == 0 || nodes[c].s == 0) {return 0;}if (nodes[i].s < nodes[c].s) {c = i;}}Remove(c);for (int i = nodes[c].d; i != c; i = nodes[i].d) {node &it = nodes[i];nodes[dep].ans = it.row;for (int j = nodes[i].r; j != i; j = nodes[j].r) {Remove(nodes[j].col);}if (Dance(dep + 1) == 1) {return 1;}for (int j = nodes[i].l; j != i; j = nodes[j].l) {Resume(nodes[j].col);}}Resume(c);return 0;}
};class Solution {
public:static void solveSudoku(vector<vector<char>> &b) {vector<vector<int>> a(9, vector<int>(9));for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {int val = b[i][j] == '.' ? 0 : int(b[i][j] - '0');a[i][j] = val;}}DLX o(a);for (int i = 0; i < 9; i++) {for (int j = 0; j < 9; j++) {b[i][j] = char(a[i][j] + '0');}}}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3029858.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

FreeRTOS任务调度器

目录 1、什么是任务调度器 2、FreeRTOS中的任务调度器 2.1 抢占式调度 2.2 时间片调度 2.3 协作式调度 3、任务调度案例分析 3.1 实验需求 3.2 CubeMX配置 3.3 代码实现 3.3.1 uart.c 重定向printf 3.3.2 打开freertos.c并添加代码 3.3.4 代码现象 1、什么是任务调度…

7 系列 FPGA 产品介绍及选型

目录 Spartan-7 FPGAsArtix-7 FPGAsKintex-7 FPGAsVirtex-7 FPGAsFPGA芯片命名规则DSP资源BRAM资源Transceivers 资源Transceivers 总带宽I/O 个数及带宽参考文档 Spartan-7 FPGAs Artix-7 FPGAs Kintex-7 FPGAs Virtex-7 FPGAs FPGA芯片命名规则 DSP资源 BRAM资源 Transceiver…

day5Qt作业

服务器端 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);//准备组件&#xff0c;初始化组件状态this->setFixedSize(800,600);chatwidget new QListWidge…

【Linux】Linux——Centos7安装Tomcat

1.下载Tomcat 安装包 官网地址&#xff1a;Apache Tomcat - Apache Tomcat 9 Software Downloadshttps://tomcat.apache.org/download-90.cgi 2.将下载的安装包上传到 Xftp 上&#xff0c;我是直接放到 usr 下了 3.将安装包解压到 /usr/local/ tar -zxvf apache-tomcat-9.0.8…

Java+SpringBoot+JSP实现在线心理评测与咨询系统

前言介绍 随着互联网技术的高速发展&#xff0c;人们生活的各方面都受到互联网技术的影响。现在人们可以通过互联网技术就能实现不出家门就可以通过网络进行系统管理&#xff0c;交易等&#xff0c;而且过程简单、快捷。同样的&#xff0c;在人们的工作生活中&#xff0c;也就…

249 基于matlab的MED、OMEDA、MOMEDA、MCKD信号处理方法

基于matlab的MED、OMEDA、MOMEDA、MCKD信号处理方法。最小熵反褶积(MED)&#xff0c;最优最小熵反卷积调整卷积 (OMEDA),多点最优最小熵解卷积调整&#xff08;Multipoint Optimal Minimum Entropy Deconvolution Adjusted&#xff0c;MOMEDA&#xff09;&#xff0c;最大相关峭…

Neuralink首个脑机接口患者:打游戏、搞研究两不误,重获自主能力

今年1月28日&#xff0c;Neuralink首次将侵入式脑机接口植入人类患者Noland Arbaugh的大脑。100天后&#xff0c;这家由埃隆马斯克创立的公司公布了最新的进展。从Neuralink的更新中我们可以看到&#xff0c;Arbaugh的恢复情况超出预期&#xff0c;他的用户体验也非常积极。 原…

Android 按钮Button点击音效

一、新建工程 编译运行&#xff0c;确保工程无误&#xff0c;这里不过多赘述。 二、UI布局 添加两个播放音效Button <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"…

芋道系统springcloud模块启动报错,枚举类不能为空

问题描述&#xff1a; Error starting ApplicationContext. To display the conditions report re-run your application with debug enabled. 2024-05-10 15:50:15.756 | ERROR 9120 | main [TID: N/A] o.s.b.d.LoggingFailureAnalysisReporter | ************************…

腾讯云coding代码托管平台配置问题公钥拉取失败提示 Permission denied(publickey)

前言 最近在学校有个课设多人开发一个游戏&#xff0c;要团队协作&#xff0c;选用了腾讯云的coding作为代码管理仓库&#xff0c;但在配置的时候遇到了一些问题&#xff0c;相比于github&#xff0c;发现腾讯的coding更难用&#xff0c;&#xff0c;&#xff0c;这里记录一下…

【漫画版】指挥官的排序战术:快速排序算法解密

作者介绍&#xff1a;10年大厂数据\经营分析经验&#xff0c;现任字节跳动数据部门负责人。 会一些的技术&#xff1a;数据分析、算法、SQL、大数据相关、python&#xff0c;欢迎探讨交流 欢迎加入社区&#xff1a;码上找工作 作者专栏每日更新&#xff1a; LeetCode解锁1000题…

stm32单片机学习路线

第一步 编程及硬件基础知识 1.掌握C语言基础 作为STM32的主要编程语言&#xff0c;C语言的基础知识是必不可少的。建议通过书籍、在线课程或者教学视频系统地学习C语言的基础知识&#xff0c;包括语法、数据类型、函数、指针等。 2.了解电子电路基础 对于单片机开发来说&…

面向对象 03:类与对象的创建、初始化和使用,通过 new 关键字调用构造方法,以及创建对象过程的内存分析

一、前言 记录时间 [2024-05-10] 系列文章简摘&#xff1a; Java 笔记 01&#xff1a;Java 概述&#xff0c;MarkDown 常用语法整理 Java 笔记 11&#xff1a;Java 方法相关内容&#xff0c;方法的设计原则&#xff0c;以及方法的定义和调用 面向对象 01&#xff1a;Java 面向对…

ESP32引脚入门指南(四):从理论到实践(PWM)

引言 ESP32 作为物联网领域的明星微控制器&#xff0c;除了强大的Wi-Fi和蓝牙功能&#xff0c;还内置了丰富的外设资源&#xff0c;其中就包括高级的PWM&#xff08;脉冲宽度调制&#xff09;功能。本文将深入探讨ESP32的PWM引脚&#xff0c;解析其工作原理&#xff0c;并通过…

2024年数维杯B题完整代码和思路论文讲解与分析

2024数维杯数学建模完整代码和成品论文已更新&#xff0c;获取↓↓↓↓↓ https://www.yuque.com/u42168770/qv6z0d/bgic2nbxs2h41pvt?singleDoc# 2024数维杯数学建模B题45页论文和代码已完成&#xff0c;代码为全部问题的代码 论文包括摘要、问题重述、问题分析、模型假设、…

G2 - 人脸图像生成(DCGAN)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 目录 理论知识DCGAN原理 模型结构逻辑结构物理结构 模型实现前期准备1. 导入第三方库2. 修改随机种子(相同的随机种子&#xff0c;第i次随机的结果是固定的)3.…

Cocos creator实现《战机长空》关卡本地存储功能

Cocos creator实现《战机长空》关卡本地存储功能 Cocos creator在开放小游戏过程中&#xff0c;经常会出现设置关卡&#xff0c;这里记录一下关卡数据本地存储功能。 一、关卡设置数据 假如我们有关卡数据如下&#xff0c; let settings [ { level: 1, // 第1关 score: 0,…

SAM轻量化应用Auto-SAM、Group-Mix SAM、RAP-SAM、STLM

1. Auto SAM&#xff08;Auto-Prompting SAM for Mobile Friendly 3D Medical Image Segmentation&#xff09; 1.1 面临问题 医学背景&#xff1a; &#xff08;1&#xff09;与自然图像相比&#xff0c;医学图像的尺寸更小&#xff0c;形状不规则&#xff0c;对比度更低。&…

window10设置静态IP

右键桌面网络图标 点击属性 点击要查看的网络 点击详细信息 获得网络连接详细信息 右键WiFi符号 或者其他方式进入网络与internet中心 点击 WLAN 点击属性 点击编辑&#xff08;点击一个即可&#xff09; 选择手动将刚才的信息方进入即可 完成

多模态中的“单流模型”和“双流模型”

多模态预训练模型按照模型结构可以分为单流和双流两种结构。 单流是指图片和文本在embedding之后就融合在一起进入后续的transformer层。【先将信息fusion&#xff0c;然后再用一个model处理】双流是指文本和图片单独享有自己的transformer层&#xff0c;只在最后做轻量的融合…