<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>lzl&apos;s blog</title><description>算法与技术小站~</description><link>https://lzleureka.cn/</link><language>zh_CN</language><item><title>Java 笔记 (chapter 02)</title><link>https://lzleureka.cn/posts/engineering/java/chapter2/</link><guid isPermaLink="true">https://lzleureka.cn/posts/engineering/java/chapter2/</guid><description>Java基础系列 个人整理笔记 方便个人回忆知识点 该章节涉及运算符与其运算优先级，标识符规则，进制表示，控制结构（if-else、switch、for、while、do-while、break、continue、return）的主要细节</description><pubDate>Thu, 30 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;接 chapter 1 的 Java 基础知识&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Java 基础知识&lt;/h1&gt;
&lt;h2&gt;⭐运算符&lt;/h2&gt;
&lt;h3&gt;运算符介绍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;算术运算符&amp;lt;br&amp;gt;
+(正) -(负) +(加) -(减) *(乘)&amp;lt;br&amp;gt;
/(除) %(取余)&amp;lt;br&amp;gt;
++(自增) --(自减) +(字符串拼接)&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关系运算符 (比较运算符)&amp;lt;br&amp;gt;
== $\ \ $ != $\ \ $ &amp;lt; $\ \ $ &amp;gt; $\ \ $ &amp;lt;= $\ \ $ &amp;gt;= $\ \ $ instanceof &amp;lt;br&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑运算符&amp;lt;br&amp;gt;
&amp;amp;&amp;amp; $\ \ $ || $\ \ $ &amp;amp; $\ \ $ | $\ \ $ ! $\ \ $ ^&amp;lt;br&amp;gt; 短路与（&amp;amp;&amp;amp;），短路或（||），逻辑非（!），逻辑与（&amp;amp;），逻辑或（|），^（逻辑异或）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;赋值运算符&amp;lt;br&amp;gt;
= $\ \ $ +=  $\ \ $ -= $\ \ $ *= $\ \ $  /= $\ \ $ %= &amp;lt;br&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;位运算符&amp;lt;br&amp;gt;
| $\ \ $ &amp;amp;  $\ \ $ &amp;gt;&amp;gt; $\ \ $ &amp;lt;&amp;lt; $\ \ $ &amp;gt;&amp;gt;&amp;gt; $\ \ $&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三元运算符&amp;lt;br&amp;gt;
a &amp;gt; b ? a : b&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
取模本质：a % b = a - a / b * b
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System.out.println(10 % 3); // 1
System.out.println(-10 % 3); // -1
System.out.println(10 % -3); // 1
System.out.println(-10 % -3); // -1

System.out.println(-10.5 % 3); // -1.5  
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important
观察可发现，&lt;strong&gt;结果的正负值与被除数一致&lt;/strong&gt;；如果a为小数,a%b=a-(int)a/b*b;
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关于自增自减的细节&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 规则就是使用临时变量
int i = 1;
i = i ++ ; // (1) tmp = i; (2)i = i + 1; (3) i = tmp;
System.out.println(i); // 1 

int i = 1;
i = ++i; // (1)i = i + 1; (2) tmp = i; (3) i = tmp;
System.out.println(i); // 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;逻辑运算和位运算&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&amp;amp; $\ \ $ &amp;amp;&amp;amp;&amp;lt;br&amp;gt;
| $\ \ $ ||&amp;lt;br&amp;gt;
除了本来的逻辑判断外,&amp;amp;&amp;amp; 和||在判断过程中有短路的特性,&amp;lt;br&amp;gt;
(条件1)&amp;amp;&amp;amp;(条件2) 只要条件1为false,则结果为false,条件2判断式不再执行&amp;lt;br&amp;gt;
(条件1)||(条件2) 只要条件1为true,则结果为true,条件2判断式不再执行&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a=1
if((1&amp;gt;2)&amp;amp;&amp;amp;(a++&amp;gt;2))
{
    pass;
}
sout(a);
//a=1
=============================
a=1
if((1&amp;lt;2)||(a++&amp;gt;2))
{   
    pass;
}
sout(a);
//a=1

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而逻辑与（&amp;amp;）和逻辑或（&amp;amp;）则没有短路的特性&amp;lt;br&amp;gt;
(条件1)&amp;amp;(条件2) 无论条件1结果为T/F,条件2判断式都会执行&amp;lt;br&amp;gt;
(条件1)|(条件2) 无论条件1结果为T/F,条件2判断式都会执行&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a=1
if((1&amp;gt;2)&amp;amp;(a++&amp;gt;2))
{
    pass;
}
sout(a);
//a=2
=============================
a=1
if((1&amp;lt;2)|(a++&amp;gt;2))
{   
    pass;
}
sout(a);
//a=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;赋值运算符 =&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顺序从左往右&lt;/li&gt;
&lt;li&gt;左侧为变量,右侧可以是变量,表达式,常量值 a=b;a=10-3*2;a=20;&lt;/li&gt;
&lt;li&gt;a+=3;等价于a=a+3&lt;/li&gt;
&lt;li&gt;复合赋值运算会进行类型转换&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;byte b=2;
b+=3;
//等价于 b=(byte)b+3;
//如果写b=b+3会报错;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;运算符优先级&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;优先级&lt;/th&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;结合性（与操作数）&lt;/th&gt;
&lt;th&gt;目数&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;点&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;()&lt;/td&gt;
&lt;td&gt;圆括号&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;[]&lt;/td&gt;
&lt;td&gt;方括号&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;正号&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;单目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;负号&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;单目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;++&lt;/td&gt;
&lt;td&gt;自增&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;单目&lt;/td&gt;
&lt;td&gt;前缀增，后缀增&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;自减&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;单目&lt;/td&gt;
&lt;td&gt;前缀减，后缀减&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;~&lt;/td&gt;
&lt;td&gt;按位取反符&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;单目&lt;/td&gt;
&lt;td&gt;适用于整数类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;!&lt;/td&gt;
&lt;td&gt;逻辑非运算符&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;单目&lt;/td&gt;
&lt;td&gt;适用于布尔类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;乘&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;除&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;除号两边是整型数据为整数除法：取商的整数部分，小数部分去掉，不四舍五入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;取余&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;+&lt;/td&gt;
&lt;td&gt;加&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;减&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&amp;lt;&amp;lt;&lt;/td&gt;
&lt;td&gt;左移位运算符&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;右移位运算符&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;无符号右移&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&amp;lt;&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt;=&lt;/td&gt;
&lt;td&gt;小于或等于&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&amp;gt;=&lt;/td&gt;
&lt;td&gt;大于或等于&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;instanceof&lt;/td&gt;
&lt;td&gt;确定某对象是否属于指定的类或者子类&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;==&lt;/td&gt;
&lt;td&gt;等于&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;!=&lt;/td&gt;
&lt;td&gt;不等于&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&amp;amp;&lt;/td&gt;
&lt;td&gt;逻辑与&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;|&lt;/td&gt;
&lt;td&gt;逻辑或&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;^&lt;/td&gt;
&lt;td&gt;逻辑异或&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&amp;amp;&amp;amp;&lt;/td&gt;
&lt;td&gt;短路与&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;||&lt;/td&gt;
&lt;td&gt;短路或&lt;/td&gt;
&lt;td&gt;从左至右&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;? :&lt;/td&gt;
&lt;td&gt;条件运算符&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;三目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;=&lt;/td&gt;
&lt;td&gt;赋值运算符&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;+= 或者 -= 或者 *=  或者 /= 或者 %= 或者 &amp;gt;&amp;gt;=  或者 &amp;lt;&amp;lt;= 或者&amp;gt;&amp;gt;&amp;gt;= 或者  &amp;amp;= 或者 |= 或者 ^=&lt;/td&gt;
&lt;td&gt;混合赋值运算符&lt;/td&gt;
&lt;td&gt;从右至左&lt;/td&gt;
&lt;td&gt;双目&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;单目乘除为关系，逻辑三目后赋值&amp;lt;br&amp;gt;
1点,括号2单目3算术4位移5比较6逻辑7三元8赋值&lt;/p&gt;
&lt;h2&gt;标识符&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;大小英文字母，数字下划线和$&lt;/li&gt;
&lt;li&gt;数字不能开头&lt;/li&gt;
&lt;li&gt;不能包含关键字和保留字&lt;/li&gt;
&lt;li&gt;严格区别大小写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;保留字：byValue, cast, future, generic, inner, operator, outer, rest, var, goto, const&lt;/p&gt;
&lt;h3&gt;标识符命名规范&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;包名：多单词组成时所有字母都小写：aaa.bbb.ccc // com.lzl.spring&lt;/li&gt;
&lt;li&gt;类名、接口名：多单词组成时，所有单词的首字母大写：XxxYyyZzz【大驼峰】&lt;/li&gt;
&lt;li&gt;变量名、方法名：多单词组成时，第一个首字母小写，第二个单词开始每个单词首字母大写&lt;/li&gt;
&lt;li&gt;常量名：所有字母大写。多单词时每个单词用下划线链接：XXX_YYY_ZZZ&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;包名 aaa.bbb.ccc&amp;lt;br&amp;gt;
com.zbx.stu&lt;/li&gt;
&lt;li&gt;类名,接口名 XxxYyyZxx&amp;lt;br&amp;gt;
TankShotGame&lt;/li&gt;
&lt;li&gt;变量名,方法名 xxxYyyZzz&amp;lt;br&amp;gt;
peopleName&lt;/li&gt;
&lt;li&gt;常量名 XXX_YYY_ZZZ&amp;lt;br&amp;gt;
COLUMN_LONG 列长度&amp;lt;br&amp;gt;
ROW_LONG 行长度&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;⭐进制表示&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;二进制 0b 0B&amp;lt;br&amp;gt;
0b1010 $\rightarrow$ 10&lt;/li&gt;
&lt;li&gt;八进制 0&amp;lt;br&amp;gt;
011 $\rightarrow$ 9&lt;/li&gt;
&lt;li&gt;十进制&amp;lt;br&amp;gt;
10 $\rightarrow$ 10&lt;/li&gt;
&lt;li&gt;十六进制 0x 0X&amp;lt;br&amp;gt;
0x11 $\rightarrow$ 17&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;位运算&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;~2 按位取反&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2&amp;amp;3 按位与&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;2|3 按位或&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;gt;&amp;gt; 算术右移:符号位不变,低位溢出,符号位补溢出的高位&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;&amp;lt; 算术左移:符号位不变,低位补0&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&amp;gt;&amp;gt;&amp;gt; 逻辑右移,也叫无符号右移:低位溢出,高位补0&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;&amp;lt;&amp;lt; 没有这个&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;原码反码补码&lt;/h3&gt;
&lt;p&gt;在java当中 &lt;strong&gt;随机一个整数默认int,随机一个小数默认double&lt;/strong&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最高位是符号位 0 正数,1负数&lt;/li&gt;
&lt;li&gt;正数: 原码反码补码保持一致&lt;/li&gt;
&lt;li&gt;负数: 反码 符号位不变 其他取反&lt;/li&gt;
&lt;li&gt;负数: 补码=反码+1，反码=补码-1 （貌似补码的反码+1是原码）&lt;/li&gt;
&lt;li&gt;0: 反码,补码都是0&lt;/li&gt;
&lt;li&gt;java没有unsigned 没有无符号数&lt;/li&gt;
&lt;li&gt;计算机运算时,&lt;strong&gt;以补码方式运算&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;人类看结果,为原码&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;+2的原码与及其的补码是一致的&amp;lt;br&amp;gt;
00000000 00000000 00000000 00000010&amp;lt;br&amp;gt;
-3的原码如下：&amp;lt;br&amp;gt;
&lt;strong&gt;1&lt;/strong&gt;0000000 00000000 00000000 00000011&amp;lt;br&amp;gt;
-3的反码如下：&amp;lt;br&amp;gt;
&lt;strong&gt;1&lt;/strong&gt;1111111 11111111 11111111 11111100&amp;lt;br&amp;gt;
-3的补码如下:&lt;br /&gt;
&lt;strong&gt;1&lt;/strong&gt;1111111 11111111 11111111 11111101&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::tip
如果给定一个int范围内的二进制数，如何求其十进制为多少？&amp;lt;br&amp;gt;
解答：细节1   必须要有32位才可能是负数&amp;lt;br&amp;gt;
细节2   负数处理：假设负数是 x ， 则 x 的带符号反码 + 1 = (-x)， 因此， x 的带符号反码 = (-x)-1，所以 x = （-1）* （x 的带符号反码） - 1 &amp;lt;br&amp;gt;
为什么需要 x 的带符号反码来处理？因为 x 的带符号反码的首位是最高位是0，可以按照正数的方式处理 &amp;lt;br&amp;gt;
为什么不是 x 的带符号反码 + 1 之后再乘 -1 ，因为如果 x = -2147483648，则（-x）int 范围无法表示
:::&lt;/p&gt;
&lt;p&gt;:::note
随堂小测&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int i = 66;
System.out.println(++i + i); // 134
double num = 3d; // 正确，3d 表示 double 类型的 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;控制结构&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;顺序&lt;/li&gt;
&lt;li&gt;分支(if else switch)&lt;/li&gt;
&lt;li&gt;循环(for,while,do while)&lt;/li&gt;
&lt;li&gt;break&lt;/li&gt;
&lt;li&gt;continue&lt;/li&gt;
&lt;li&gt;return&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;程序流程控制&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;顺序控制&lt;/li&gt;
&lt;li&gt;分支控制&lt;/li&gt;
&lt;li&gt;循环控制&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;顺序控制&lt;/h4&gt;
&lt;p&gt;正常的从上往下的程序流程&lt;/p&gt;
&lt;h4&gt;分支控制&lt;/h4&gt;
&lt;p&gt;分支控制结构有单分支结构（只有一个 if 结构）、双分支结构（有 if-else 两个结构）、多分支结构（如下面结构所示）以及下一小小小节的嵌套分支结构，再加上switch-case结构的分支结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(条件表达式){
    代码块;
}
else if{
    代码块;
}
......
else{
    代码块;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多分支最后 可以没有else&lt;/p&gt;
&lt;h5&gt;嵌套分支结构&lt;/h5&gt;
&lt;p&gt;嵌套分支结构,在一个分支结构当中嵌套了另一个完整的分支结构.&amp;lt;br&amp;gt;
规范:不要超过3层,可读性不好&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if(){
    if(){
        //if else
    }else{
        //if else
    }
}else{
    //if else
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;switch-case&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;switch(表达式){
    case 常量1:   //当...,执行语句
        语句块1:
        break;    //退出switch
    case 常量2:
        语句块2: 
        break;  
    case 常量3:
        语句块3:
        break;
        
        ......    
    case 常量n:
        语句块n: 
        break;
    default:
        default语句块;
        break;  
  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;switch注意事项&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;表达式数据类型应该和case之后的常量&lt;strong&gt;类型一致&lt;/strong&gt;，或者是&lt;strong&gt;可以自动转换可以相互比较的类型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;siwtch(表达式)中表达式的返回值必须是(byte,short,int,char,enum(枚举),String)&lt;/li&gt;
&lt;li&gt;case子句的值必须是常量&lt;/li&gt;
&lt;li&gt;default子句是可选的，没有匹配的case时，执行default&lt;/li&gt;
&lt;li&gt;break语句用来执行完一个case分支后，使程序跳出switch，如果没有break，则会顺序执行到switch结尾（这个时候是不会走case，直接走对应的语句块），除非遇到一个break&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::note
第一点的意思是假设表达式为 char，case子句中是 int，这样是可以的，例子如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char c = &apos;a&apos;;
switch(c) {
  case &apos;a&apos;: 
    break;
  case 20:
    break;
  default:
    break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;default和break的三种特殊情况&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第一种
switch (i) {     
    default:     
         System.out.println(&quot;默认&quot;);    
    case 1:     
         System.out.println(&quot;1&quot;);     
    case 2:     
         System.out.println(&quot;2&quot;);   
    case 3:     
         System.out.println(&quot;3&quot;);     
} 
当给定义i=1时，输出的结果为： 结果为 1,2,3 
当给定义i=2时，输出的结果为： 结果为 2,3
当给定义i=3时，输出的结果为： 结果为 3
当给定义i=4时，输出的结果为： 结果为 默认，1,2,3

当i匹配到的时候它会走到匹配的位置，而且还会继续运行之后的代码，直到最后位置，当匹配不到是，他会都输出一遍
=========================================
第二种
switch (i) {        
    case 1:     
         System.out.println(&quot;1&quot;);   
    default:     
         System.out.println(&quot;默认&quot;);   
    case 2:     
         System.out.println(&quot;2&quot;);   
    case 3:     
         System.out.println(&quot;3&quot;);     
} 
当给定义i=1时，输出的结果为： 结果为 1,默认,2,3 
当给定义i=2时，输出的结果为： 结果为 2,3
当给定义i=3时，输出的结果为： 结果为 3
当给定义i=4时，输出的结果为： 结果为  默认,2,3

当i匹配到的时候它会走到匹配的位置，而且还会继续运行之后的代码，直到最后位置，当匹配不到是，他会从default起到以下都输出一遍
===========================================
第三种
switch (i) {        
    case 1:     
         System.out.println(&quot;1&quot;);     
    case 2:     
         System.out.println(&quot;2&quot;);   
    case 3:     
         System.out.println(&quot;3&quot;);   
    default:     
         System.out.println(&quot;默认&quot;);   
} 
当给定义i=1时，输出的结果为： 结果为 1，2,3，默认 
当给定义i=2时，输出的结果为： 结果为 2,3，默认 
当给定义i=3时，输出的结果为： 结果为 3，默认
当给定义i=4时，输出的结果为： 结果为  默认

当i匹配到的时候它会走到匹配的位置，而且还会继续运行之后的代码，直到最后位置，当匹配不到是，他只会输出default下的语句
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;循环控制&lt;/h4&gt;
&lt;p&gt;for循环&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;循环条件是返回一个boolean值的表达式 i&amp;lt;=10&lt;/li&gt;
&lt;li&gt;for(初始化;循环条件;变量迭代)当中的初始化和变量迭代可以写到其他地方&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int i=0;
for( ;i&amp;lt;=10; ){
    sout(&quot;hello&quot;);
    i++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;初始化值可以是多个，但类型要求一样，之间用”,“分隔开,变量迭代也一样&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;for(int i=0,j=1;i&amp;lt;=10;i++,j+=2){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;for循环顺序&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;for({1};{2};{3})
{
    {4};
}
执行顺序为1,2,4,3;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;for 中 i++ 和 ++i 的区别&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;i++ ++i 效果一样&amp;lt;br&amp;gt;
:::important
++i数据多的时候比i++更快,因为 i++ 需要临时变量
:::&lt;/p&gt;
&lt;p&gt;while循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始化变量;
while(循环条件){
    循环体;
    变量迭代;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;do while循环&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始化变量;
do{
    循环体;
    变量迭代;
} while(循环条件);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;break&lt;/h3&gt;
&lt;p&gt;跳出循环&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用标签时，可以指定跳出那一层循环&lt;/li&gt;
&lt;li&gt;label1:是标签，可以随意指定，修改&lt;/li&gt;
&lt;li&gt;break label2:跳出label2层，可以指定&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尽量不使用标签&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;break之后没有标签，默认跳出最近的一层&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//break 可以使用标签，指定终止那一层
label1:
for (int i = 1; i &amp;lt;= 5; i++) {
    label2:
    for (int j = 1; j &amp;lt;= 3; j++) {
        System.out.println(i + &quot; &quot; + j);
        if (i == 2) {
            // break label1;
            break label2;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;continue&lt;/h3&gt;
&lt;p&gt;跳出当前循环，再进行循环,进行下一次的循环判断&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;continue也可以使用标签，和break用法一致&lt;/li&gt;
&lt;li&gt;continue没有标签，跳过最近的循环&lt;/li&gt;
&lt;li&gt;continue 的标签也尽量少使用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;return&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;跳出方法，返回值&lt;/li&gt;
&lt;li&gt;用在main时，退出程序&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;第二章暂时讲到这，这章重点为运算符与进制表示&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Java 笔记 (chapter 01)</title><link>https://lzleureka.cn/posts/engineering/java/chapter1/</link><guid isPermaLink="true">https://lzleureka.cn/posts/engineering/java/chapter1/</guid><description>Java基础系列 个人整理笔记 方便个人回忆知识点 该章节涉及Java历史，Java中jdk，jre和jvm的关系，Java变量，Java数据类型和类型转换</description><pubDate>Sun, 26 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Java历史&lt;/h1&gt;
&lt;p&gt;1995 v1&lt;/p&gt;
&lt;p&gt;Java se  $\ \ \ $   标准版&lt;/p&gt;
&lt;p&gt;Java ee   $\ \ \ $  企业版&lt;/p&gt;
&lt;p&gt;Java me(Micro Edition)   $\ \ \ $  小型版  $\ \ \ $ 基本不用&lt;/p&gt;
&lt;h1&gt;Java特点&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;面向对象&lt;/li&gt;
&lt;li&gt;强类型机制，异常处理，垃圾自动回收&lt;/li&gt;
&lt;li&gt;跨平台&lt;/li&gt;
&lt;li&gt;解释性语言(也有 jvm 中热点代码先编译再执行)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Java 的安全性、健壮性和严谨性是强类型语言的一重要体现；&lt;/li&gt;
&lt;li&gt;Java的强类型语言对数据类型兼容性的要求比任何语言都要严格；&lt;/li&gt;
&lt;li&gt;每个变量有类型，每个表达式有类型，而且每种类型是严格定义的。其次，所有的数值传递，不管是直接的还是通过方法调用经由参数传过去的都要先进行类型相容性的检查；&lt;/li&gt;
&lt;li&gt;Java 编译器对所有的表达式和参数都要进行类型相容性的检查以保证类型是兼容的。任何类型的不匹配都是错误的，在编译器完成编译以前，错误必须被改正；&lt;/li&gt;
&lt;li&gt;初期你可能发现 Java 的强制类型检查有点繁烦。但是要记住，从长远来说它将帮助你减少程序出错的可能性。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;解释性语言 js,Java &amp;lt;br&amp;gt;
编译性语言 c c++ &amp;lt;br&amp;gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;区别&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解释性语言，编译后的代码，不能被机器直接执行，需要解释器.&amp;lt;br&amp;gt;&lt;/li&gt;
&lt;li&gt;编译性语言,编译后的代码(bin)，可以直接被机器执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;JVM &amp;amp; JDK &amp;amp; JRE 之间的关系&lt;/h1&gt;
&lt;h2&gt;JVM(Java Virtual Machine)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JVM   虚拟计算机，具有指令集并使用不同的存储区域，复制执行指令，管理数据，内存，寄存器，包含在JDK当中&lt;/li&gt;
&lt;li&gt;不同的平台，有不同的虚拟机&lt;/li&gt;
&lt;li&gt;Java虚拟机 机制屏蔽了底层运行平台的差异，实现一次编译，到处运行&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;JRE介绍&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JRE(Java runtime environment      Java运行环境)&lt;/li&gt;
&lt;li&gt;JRE=JVM + Java核心类库&lt;/li&gt;
&lt;li&gt;JRE包含 JVM 和 Java 程序所需要的核心类库&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;JDK(Java Development Kit)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JDK —— Java开发工具包&lt;/li&gt;
&lt;li&gt;JDK = JRE + Java的开发工具(Java Javac Javadoc Javap等)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;JDK= JRE + Java开发工具&amp;lt;br&amp;gt;
JRE = JVM+Java核心类库&amp;lt;br&amp;gt;
JDK = JRE ( JVM + 核心类库 ) + Java开发工具&lt;/p&gt;
&lt;h1&gt;Java执行流程&lt;/h1&gt;
&lt;p&gt;.java文件(源文件)------&amp;gt;.class文件(字节码文件)------&amp;gt;结果&lt;/p&gt;
&lt;p&gt;javac编译&lt;/p&gt;
&lt;p&gt;Java装载到JVM执行&lt;/p&gt;
&lt;h1&gt;Java开发细节&lt;/h1&gt;
&lt;p&gt;一个Java源文件&lt;strong&gt;可以有多个class类,但最多一个public类&lt;/strong&gt;&amp;lt;br&amp;gt;
一个源文件最多一个public类，其他类不限，&lt;strong&gt;可以将main方法写在非public类当中&lt;/strong&gt;,
然后&lt;strong&gt;指定运行非public类，这样入口方法就是非public的main方法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HelloWorld {
    //有几个class类 编译之后就有几个.class 文件
    //可以使用Java dog单独运行dog.class或cat.class
    public static void main(String[] args) {
        System.out.println(&quot;hello world!&quot;);
    }
}
class dog {
    public static void main(String[] args) {
        System.out.println(&quot;dog&quot;);
    }
}
class cat {
    public static void main(String[] args) {
        System.out.println(&quot;cat&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
一个文件里面只有一个 public 类，和其他没有修饰词的类&lt;/p&gt;
&lt;p&gt;并且这些类里面都可以有main函数，并且可以自己主动调用别的函数的main函数（类名.main）
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;面对不断变化的新世界，我们该如何应对？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;如何学习新技术&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;需求&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;工作需要&lt;/li&gt;
&lt;li&gt;跳槽，对方要求&lt;/li&gt;
&lt;li&gt;技术控&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能否用传统技术解决？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;能解决，但不完美&lt;/li&gt;
&lt;li&gt;解决不了&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;时刻询问自己，新技术到底给我带来了什么优势？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;引出我们学习的新技术和知识点&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;学习新技术或者知识点的基本语句和基本语法（不要考虑细节）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;快速入门（基本程序，crud）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开始考虑研究技术的注意事项，使用细节，使用规范，如何优化&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	优化无止境，技术魅力&lt;/p&gt;
&lt;h3&gt;转义字符&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;\t    tab&lt;/li&gt;
&lt;li&gt;\n    换行&lt;/li&gt;
&lt;li&gt;\    一个\&lt;/li&gt;
&lt;li&gt;\&quot;    一个&quot;&lt;/li&gt;
&lt;li&gt;\&apos;    一个&apos;&lt;/li&gt;
&lt;li&gt;\r    一个回车&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回车和换行的区别：回车是把当前光标放到目前的第一行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 传统编译运行在终端中输出 xxxhh
// 在IDEA中输出xxx
System.out.println(&quot;hhhhh\rxxx&quot;); 
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;接下来开启基础知识的导航&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Java 基础知识&lt;/h1&gt;
&lt;h2&gt;注释&lt;/h2&gt;
&lt;p&gt;单行 //&amp;lt;br&amp;gt;
多行 /**/&lt;/p&gt;
&lt;h2&gt;文档注释&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**

* @author lzl~
* @version 1.0
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class demo2_ChangeChar {
    /**
     * @author zbx
     * @version 1.0
     * */
    public static void main(String[] args) {
        System.out.println(&quot;demo1\tdemo\tdemo&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;javadoc -d [filename] -xx -yy [javaFile]&lt;/code&gt;,可以生成一套网页文件形式的文档说明&lt;/p&gt;
&lt;p&gt;:::tip
注意 &lt;code&gt;[filename]&lt;/code&gt; 表示一个参数，这里参数表示Java文件所在文件夹的名字&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[javaFile]&lt;/code&gt; 表示 java 源文件
:::&lt;/p&gt;
&lt;p&gt;javadoc -d d://temp -xx -yy 当前java源文件&amp;lt;br&amp;gt;
xx yy 指的是使用到的标签， 即 &lt;code&gt;author&lt;/code&gt; 和 &lt;code&gt;version&lt;/code&gt; 等&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;更多的一定要看以下 javadoc 相关文档&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;https://www.cnblogs.com/linj7/p/14339381.html&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;java 规范&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;部分规范&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;类、方法的注释，要以javadoc的方式来写&lt;/li&gt;
&lt;li&gt;非javadoc的注释，往往是给代码的维护者看的，着重告诉读者为什么这么写，如何修改，注意什么问题等&lt;/li&gt;
&lt;li&gt;使用tab操作，实现缩进，默认整体向右边移动，用 shift+tab整体向左移动&lt;/li&gt;
&lt;li&gt;运算符和=习惯两边加空格&lt;/li&gt;
&lt;li&gt;源文件使用utf-8编码&lt;/li&gt;
&lt;li&gt;行宽度不要超过80字符&lt;/li&gt;
&lt;li&gt;代码编写次行风格和行尾风格(说的是花括号)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;dos 命令&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;tree [dir]
cls # 清屏
exit # 退出
md
rd
echo
type
del
copy
move
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
[dir] 表示文件目录
:::&lt;/p&gt;
&lt;h2&gt;⭐变量&lt;/h2&gt;
&lt;p&gt;变量是程序的基本组成单位&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;类型  名称     = 值&amp;lt;br&amp;gt;
int       a      =   1;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不同的类型占用不同的空间大小 8bit=1byte&lt;/p&gt;
&lt;p&gt;变量表示内存中的一个存储区域&lt;/p&gt;
&lt;p&gt;变量=变量名+值+数据类型&lt;/p&gt;
&lt;h3&gt;+号的使用&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;加号左右两边都是数值时，做加法运算&lt;/li&gt;
&lt;li&gt;加号左右&lt;strong&gt;两边有一方是字符串时，做拼接运算&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class demo3_var3 {
    public static void main(String[] args) {

        int a = 50;
        int b = 20;
        String s = &quot;hello&quot;;

        System.out.println(a + b);
        System.out.println(a + s);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;结果&amp;lt;br&amp;gt;
70&amp;lt;br&amp;gt;
50hello&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;数据类型&lt;/h3&gt;
&lt;p&gt;1,基本数据类型&amp;lt;br&amp;gt;
2,引用数据类型&lt;/p&gt;
&lt;p&gt;基本数据类型包括&lt;/p&gt;
&lt;p&gt;1数值型,2字符型,3bool型&amp;lt;br&amp;gt;
1.1 byte[1],short[2],int[4],long[8]&amp;lt;br&amp;gt;
1.2 float[4],double[8] 浮点数 = 符号位 + 指数位 + 尾数位&amp;lt;br&amp;gt;
2.1 char[2]&amp;lt;br&amp;gt;
3.1 boolean[1]&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;引用数据类型包括 1class类,2interface接口,3[]数组&lt;/p&gt;
&lt;p&gt;:::note
浮点默认double，要float后面需要加 f
:::&lt;/p&gt;
&lt;h3&gt;整型细节&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int n = 1L; // 错误，不能long类型赋值给 int 类型
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;浮点细节&lt;/h3&gt;
&lt;p&gt;浮点默认double，要float后面需要加 f&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;float num = 1.1; // 错误，得 1.1f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浮点数表示：0.512, .512,  512.0f （十进制数形式）5.12e2（科学计数法）&lt;/p&gt;
&lt;p&gt;推荐使用 double，精度更高&lt;/p&gt;
&lt;p&gt;浮点数陷阱，进行大小判断时要小心&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;double a = 2.7;
double b = 8.1 / 3;
 // b != 2.7  ×
 // Math.fabs(b - 2.7) &amp;lt; eps √
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
Java API文档：https://www.matools.com/api/java8
:::&lt;/p&gt;
&lt;h3&gt;char 细节&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;char c = 97; // 字符类型可以直接存放一个数字
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编码：Unicode（固定两个字节）, ASCII（一个字节）, utf-8（字母1字节，汉字3字节）, GBK（字母1字节，汉字2字节）, gb2312(gb2312&amp;lt;GBK), big5 (繁体中文，台湾，香港)&lt;/p&gt;
&lt;p&gt;:::important
接下来这块非常重要，基本数据类型转换和强制类型转换的规则要熟悉
:::&lt;/p&gt;
&lt;h3&gt;⭐基本数据类型转换&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;在java当中 随机一个整数默认int,随机一个小数默认double&lt;/strong&gt;&amp;lt;br&amp;gt;
精度小的类型可以自动转换为精度大的类型&amp;lt;br&amp;gt;
char--&amp;gt;int--&amp;gt;long--&amp;gt;float--&amp;gt;double&amp;lt;br&amp;gt;
byte--&amp;gt;short--&amp;gt;int--&amp;gt;long--&amp;gt;float--&amp;gt;double&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有多种数据类型混合运算时,系统将自动将所有数据转换为精度最大的数据类型,再计算&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当将精度大的数据赋值给精度小的数据类型时,会报错&lt;/strong&gt;,反之会自动转换&lt;/li&gt;
&lt;li&gt;(byte,short)和char之间不会相互自动转换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;byte,short,char 三者之间可以计算,在计算时首先转换为int类型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;boolean不参与转换&lt;/li&gt;
&lt;li&gt;自动提升原则:表达式的结果自动提升为操作数当中最大的类型（和第一条意思差不多）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;byte b = 10;
char c = b; // 错误，原因 byte 不能自动转成 char
int n = 1;
b = n; // 错误，变量赋值会根据变量类型进行判断
short s = 1;
short s2 = s + b; // 错误，byte, short,char 运算 -&amp;gt; int
byte b1 = 11;
byte b2 = b1 + b; // 错误，全部是某种类型也会变成 int 类型
boolean pass = true;
int num100 = pass; // 错误，boolean 不参与类型的自动转换
byte b4 = 1; 
short s3 = 100;
int num200 = 1;
double num300 = 1.1;
double num500 = b4 + s3 + num200 + num300 // double 类型
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important
此处知识点很好地体现了Java为何为强类型机制，因为这些类型一旦不对，就会报错
:::&lt;/p&gt;
&lt;h3&gt;强制类型转换&lt;/h3&gt;
&lt;p&gt;会造成丢失精度和溢出问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int n1 = (int) 1.9;
System.out.println(&quot;n1 = &quot; + n1);

//溢出
byte b = (byte) 2000;
System.out.println(&quot;b = &quot; + b);

结果
n1 = 1
b = -48
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;数据类型从大到小需要强制转换&lt;/li&gt;
&lt;li&gt;强转符合只针对最近的操作数有效,小括号可提高优先级&lt;/li&gt;
&lt;li&gt;char可以保存int的常量值,但不能保存int的变量值,需要强转&lt;/li&gt;
&lt;li&gt;byte和short类型在运算时,当作int类型处理&lt;/li&gt;
&lt;li&gt;byte 和 short 只能保存它们类型范围内常量值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note
关于上述第三点：&lt;code&gt;char c = 90&lt;/code&gt; 可以，但是 &lt;code&gt;int a = 90; char c = a;&lt;/code&gt; 不可
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;short s = 12; // ok
s = s - 9; // error  int -&amp;gt; short 但是 s -= 9 是正确的
byte b = 10; // ok
b = b + 11; // error int-&amp;gt;byte
b = (byte)(b + 11); // ok
char c = &apos;a&apos;; // ok
int i = 16; // ok
float d = .314F; // ok
double result = c + i + d; // ok float -&amp;gt; double
short t = s + b; // error int -&amp;gt; short
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基本数据类型和String类型的转换&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基本数据类型转String &amp;lt;br&amp;gt;
基本数据类型+ &quot;&quot;&lt;/li&gt;
&lt;li&gt;String类型转基本数据类型&amp;lt;br&amp;gt;
调用parseXXX方法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
事实上还有更多互相转换的方法
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;第一章暂时讲到这，这章重点为变量类型这块&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>第144场双周赛 &amp;&amp; 第425场周赛第三、四题</title><link>https://lzleureka.cn/posts/leetcode/the144thdoubleweekcontestandthe245thweekcontest/</link><guid isPermaLink="true">https://lzleureka.cn/posts/leetcode/the144thdoubleweekcontestandthe245thweekcontest/</guid><description>leetcode周赛第三、四题系列</description><pubDate>Mon, 25 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;该系列只写每周周赛的第三、四题。使用语言主 JAVA, 副 c++ 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;144 双周赛第三题&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;题目链接：https://leetcode.cn/problems/zero-array-transformation-iii/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;有一个长度为 $n$ 的 &lt;code&gt;nums&lt;/code&gt; 数组，然后有二维数组 &lt;code&gt;queries&lt;/code&gt; ，其中 $queries[i]$=$[l_i,r_i]$&lt;/p&gt;
&lt;p&gt;substract 操作：对于任意一个 $[l_i, r_i]$，可以选择在这个区间内每个数至多&lt;strong&gt;减 1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现需要你在 &lt;code&gt;queries&lt;/code&gt; 删除最多的区间，使得依然可以在剩余的区间里面进行 substract 操作&lt;strong&gt;使得 &lt;code&gt;nums&lt;/code&gt; 数组最终所有值为 0&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1\le n \le 10^5$&lt;/li&gt;
&lt;li&gt;$0 \le nums[i] \le 10^5$&lt;/li&gt;
&lt;li&gt;$1 \le queries.length \le 10^5$&lt;/li&gt;
&lt;li&gt;$queries[i].length == 2$&lt;/li&gt;
&lt;li&gt;$0 \le l_i \le r_i \lt n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;差分+贪心&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度$O(nlogn)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;既然题目要求的是减一，那我反转一下，对一个长度为 $n$，值全为 0 的数组 &lt;code&gt;d&lt;/code&gt; 利用 queries 数组进行加 1 操作，也就是 add 操作。&lt;/p&gt;
&lt;p&gt;先考虑无解的情况，现在如果对 queries 中的每个区间的每个数都加 1，如果存在有 &lt;code&gt;d[i] &amp;lt; nums[i]&lt;/code&gt;，则说明此时无解。&lt;/p&gt;
&lt;p&gt;换句话说，如果最后留下的区间，然后对每个区间都利用 add 操作使得最后 &lt;code&gt;d&lt;/code&gt; 数组每个数都有 $d[i] \ge nums[i]$ ，则说明留下的这组区间是满足题意的。因此，问题转化为删除最多的区间，对剩下的区间进行 add 操作，使得对一个初始值为 0 的数组最后每一个值都 $的d[i] \ge nums[i]$&lt;/p&gt;
&lt;p&gt;由于这里有每个区间都加 1 的操作，自然而言要想到差分，将区间加变成一个 $O(1)$ 的操作。&lt;/p&gt;
&lt;p&gt;接下来考虑整体怎么做？&lt;/p&gt;
&lt;p&gt;看到这个数据范围，又是区间，可以尝试想一想贪心做法，一般来说可以尝试对区间进行排序，那么按照什么进行排序？左端点还是右端点？既然无论如果 &lt;code&gt;nums[0]&lt;/code&gt; 一定会被覆盖住，不妨就就先按照左端点排序，左端点相同的情况下，自然右端点更大更好&lt;/p&gt;
&lt;p&gt;排好序之后，从左到右遍历区间，来考虑遍历的过程中会发生什么？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果当前遍历到区间 $[l_i,r_i]$ 。也就是说当前区间的左端点是 $l_i$，也就是要保证 $d[k] \ge nums[k], k\in[0,l_i-1]$，否则应该提前返回无解的情况，因此需要使用 &lt;code&gt;cur&lt;/code&gt; 来维护当前尚未满足 $d[cur] \ge nums[cur]$ 的最小值、&lt;/li&gt;
&lt;li&gt;如果 $cur == l_i$，如果此时 $d[cur] &amp;gt;= nums[cur]$，此时的区间并不会丢弃，而是应该放入候选集中，而且只用存右端点的值，并且候选集根据右端点从大到小选择，因此这里使用优先队列来维护候选集。&lt;/li&gt;
&lt;li&gt;如果 $cur &amp;lt; l_i$，说明 $d[k], k\in [cur, l_i-1]$ 只能用候选集里面的元素进行覆盖，由于  这些候选集里面左端点在区间 $[0,cur-1]$ 之间，因此可以只用对区间 $[cur, t]$ 进行 add 操作（t 是堆顶元素）&lt;/li&gt;
&lt;li&gt;这里有个小优化，往 queries 加入区间 &lt;code&gt;[n,n]&lt;/code&gt;，然后对于 $cur==l_i$ 的情形，只需要将右端点加入队中即可。因为 step3 最终会在候选集中选择合适的区间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个贪心思路的核心思想在于尽可能地使用范围比较大的区间，同时不随便放弃任意一个区间，除非 $t \lt cur$，也就是堆顶元素小于当前处理到的 &lt;code&gt;d&lt;/code&gt; 数组位置。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- 另外 &lt;code&gt;d&lt;/code&gt; 数组本质上是一个差分数组，所以在更新和判断的时候注意加上 &lt;code&gt;d[i - 1]&lt;/code&gt;。当然，也可以用一个变量优化掉 &lt;code&gt;d&lt;/code&gt; 数组 --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;c++代码如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int N = 100010;
int d[N];
class Solution {
public:
    priority_queue&amp;lt;int&amp;gt; Q;
    void init(int n) {
        for (int i = 0; i &amp;lt;= n + 2; i ++ ) d[i] = 0;
    }
    void withdraw(int cur) {
        int t = Q.top(); Q.pop();
        d[cur] ++ ; d[t + 1] -- ;
    }
    int maxRemoval(vector&amp;lt;int&amp;gt;&amp;amp; nums, vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt;&amp;amp; queries) {
        int n = nums.size();
        queries.push_back(vector&amp;lt;int&amp;gt;{n, n});
        int m = queries.size();
        init(n);

        sort(queries.begin(), queries.end(), [&amp;amp;](const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b) {
            if (a[0] == b[0]) return a[1] &amp;gt; b[1];
            return a[0] &amp;lt; b[0];
        });
        
        int cur = 1; // cur 表示当前 d[cur] 尚未 d[cur] += d[cur - 1];
        int cnt = 0; // cnt 计算需要多少数量的答案
        int q = 0;
        while (q &amp;lt; m) {
            auto &amp;amp;qe = queries[q];
            int l = qe[0], r = qe[1];
            l ++ ; r ++ ;
            while (l &amp;gt; cur) {
                
                while (!Q.empty() &amp;amp;&amp;amp; d[cur] + d[cur - 1] &amp;lt; nums[cur - 1]) {
                    if (Q.top() &amp;lt; cur) break;
                    withdraw(cur);
                    cnt ++ ;
                }
                if (d[cur] + d[cur - 1] &amp;lt; nums[cur - 1]) return -1;
                d[cur] += d[cur - 1];
                cur ++ ;
            }
            // l == cur
            Q.push(r);
            q ++ ;
        }
        // 出来 q == m， cur &amp;gt; n
        return m - cnt - 1;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;java代码如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    public int maxRemoval(int[] nums, int[][] queries) {
        int n = nums.length;
        int m = queries.length;
        List&amp;lt;Integer&amp;gt;[] list = new List[n];
        for (int i = 0; i &amp;lt; n; i ++ ) list[i] = new ArrayList&amp;lt;&amp;gt;();
        for (int[] q: queries) list[q[0]].add(q[1]);

        int pre = 0;
        int cnt = 0;
        int[] d = new int[n + 1];
        PriorityQueue&amp;lt;Integer&amp;gt; Q = new PriorityQueue&amp;lt;&amp;gt;(n, (a, b) -&amp;gt;  b - a);
        for (int i = 0; i &amp;lt; n; i ++ ) {
            
            for (Integer j: list[i]) Q.offer(j);
            while (!Q.isEmpty() &amp;amp;&amp;amp; pre + d[i] &amp;lt; nums[i]) {
                if (Q.peek() &amp;lt; i) return -1;
                int t = Q.poll();
                d[i] ++ ; d[t + 1] -- ;
                cnt ++ ;
            }
            if (pre + d[i] &amp;lt; nums[i]) return -1;
            pre += d[i];
        }
        return m - cnt;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
sort 那里语法一定要是 &lt;code&gt;const vector&amp;lt;int&amp;gt;&amp;amp; a&lt;/code&gt;，弄成引用传递，不然会超时
:::&lt;/p&gt;
&lt;h1&gt;144 双周赛第四题&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;题目链接：https://leetcode.cn/problems/find-the-maximum-number-of-fruits-collected/description/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;有一个 $n\times n$ 的二维数组，数组里面每个数都有一个权值。现有三个起点，分别为 $(0,0),(0,n-1),(n-1,0)$，从这三个起点出发只走 $n - 1$ 步，并且最终要达到 $(n-1,n-1)$，在移动过程中可以累加路径上的权值，但是二维数组里面的每个数（权值）只能相加一次。问：满足题意的最大权值之和？&lt;/p&gt;
&lt;p&gt;另外：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;起点为 $(0,0)$ 只能走 $(i+1,j+1),(i+1,j),(i,j+1)$ 这三个方向&lt;/li&gt;
&lt;li&gt;起点为 $(n-1,0)$ 只能走 $(i,j+1),(i-1,j+1),(i+1, j+1)$ 这三个方向&lt;/li&gt;
&lt;li&gt;起点为 $(0, n-1)$ 只能走 $(i+1, j),(i+1,j+1),(i+1,j-1)$ 这三个方向&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fruits&lt;/code&gt; 表示二维数组&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$2 \le n = fruits.length=fruits[i].length\le 1000$&lt;/li&gt;
&lt;li&gt;$0 \le fruits[i][j] \le 1000$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;动态规划&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度$O(n^2)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注意到这三个起点到 $(n-1,n-1)$ 都只能走 $n-1$ 步&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于 $(0,0)$，根据题意的走法，只能一直走 $(i+1,j+1)$ 方向才能恰好 $n-1$ 步到达终点&lt;/li&gt;
&lt;li&gt;对于 $(n-1,0),(0,n-1)$ 事实上可以将其理解为经典题目数字三角形，甚至比数字三角形更简单，起点和终点方向此处是一致的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于该题，三个起点可以分别计算，起点为 $(0,0)$ 的可以提前对角线的权值累加，并且置为 0，那么同时根据题目特性，$(n-1,0),(0,n-1)$ 到达终点的全部路径，除了可能在对角线位置有交集，其余位置没有交集，因此可以独立计算。&lt;/p&gt;
&lt;p&gt;而正好后两种情况是可以看作一个数字三角形的题来解，因此使用动态规划求解即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    static final int INF = 0x3f3f3f3f;
    int[][] f, g;
    public int maxCollectedFruits(int[][] fruits) {
        int ans = 0;
        int n = fruits.length;

        for (int i = 0; i &amp;lt; n; i ++ ) {
            ans += fruits[i][i];
                System.out.println(fruits[i][i]);
            
            fruits[i][i] = 0;
        }
        f = new int[n][n];
        g = new int[n][n];

        for (int i = 0; i &amp;lt; n; i ++ ) Arrays.fill(f[i], -INF);
        f[n - 1][0] = fruits[n - 1][0];
        for (int i = 1; i &amp;lt; n; i ++ ) { // 列
            for (int j = 0; j &amp;lt; n; j ++ ) { // 行
                if (j - 1 &amp;gt;= 0) f[j][i] = Math.max(f[j][i], f[j - 1][i - 1] + fruits[j][i]);
                f[j][i] = Math.max(f[j][i], f[j][i - 1] + fruits[j][i]);
                if (j + 1 &amp;lt; n) f[j][i] = Math.max(f[j][i], f[j + 1][i - 1] + fruits[j][i]);
            }
        }
        ans += f[n - 1][n - 1];

        for (int i = 0; i &amp;lt; n; i ++ ) Arrays.fill(g[i], -INF);
        g[0][n - 1] = fruits[0][n - 1];
        for (int i = 1; i &amp;lt; n; i ++ ) { // 行
            for (int j = 0; j &amp;lt; n; j ++ ) { // 列
                if (j - 1 &amp;gt;= 0) g[i][j] = Math.max(g[i][j], g[i - 1][j - 1] + fruits[i][j]);
                g[i][j] = Math.max(g[i][j], g[i - 1][j] + fruits[i][j]);
                if (j + 1 &amp;lt; n) g[i][j] = Math.max(g[i][j], g[i - 1][j + 1] + fruits[i][j]);
            }
        }
        ans += g[n - 1][n - 1];
        return ans;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
这道题事实上可以通过旋转二维数组做到只计算一次 dp， hh~
:::&lt;/p&gt;
&lt;h1&gt;第 425 场周赛第三题&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;题目链接：https://leetcode.cn/problems/minimum-array-sum/description/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;给定一个整数数组 &lt;code&gt;nums&lt;/code&gt; 和三个整数 $k^\prime$,&lt;code&gt;op1&lt;/code&gt;,&lt;code&gt;op2&lt;/code&gt;
可以对 nums 执行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作1：选择一个下标 &lt;code&gt;i&lt;/code&gt;，将 &lt;code&gt;nums[i]&lt;/code&gt; 除以 2，并&lt;strong&gt;向上取整&lt;/strong&gt;。可以最多执行该操作 &lt;code&gt;op1&lt;/code&gt; 次，并且每个下标最多只能执行&lt;strong&gt;一次&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;操作2：选择一个下标 &lt;code&gt;i&lt;/code&gt;，仅当 &lt;code&gt;nums[i]&lt;/code&gt; 大于或者等于 $k^\prime$ 时，从 &lt;code&gt;nums[i]&lt;/code&gt; 中减去 $k^\prime$ 。最多可以执行该操作 &lt;code&gt;op2&lt;/code&gt; 次，并且每个下标最多只能执行&lt;strong&gt;一次&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两种操作可以应用于同一下标，但每种操作最多只能应用一次。&lt;/p&gt;
&lt;p&gt;返回在执行任意次数的操作后，&lt;code&gt;nums&lt;/code&gt; 中所有元素的最小可能 &lt;strong&gt;和&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1\le nums.length \le 100$&lt;/li&gt;
&lt;li&gt;$0 \le nums[i] \le 10^5$&lt;/li&gt;
&lt;li&gt;$0 \le $k^\prime$ \le 10^5$&lt;/li&gt;
&lt;li&gt;$0 \le op1,op2 \le nums.length$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;动态规划&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度$O(n\cdot op1 \cdot op2)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这道题读完题之后，再看数据范围，就感觉可能是动态规划，猜出状态为 $f(i,j,k)$ 表示在数组 $nums[1-i]$ 中最多进行 $j$ 次操作1 和最多进行 $k$ 次操作 2 的所有最小可能和‘&lt;/p&gt;
&lt;p&gt;因此，最终答案就是 $f(n, op1, op2)$ ，$n$ 是数组的长度&lt;/p&gt;
&lt;p&gt;现在开始进行集合划分，也就是递推关系式的思考。&lt;/p&gt;
&lt;p&gt;首先初始化 $f$，也就是说 $f(i,j,k)$ 等于前 $i$ 个元素的和，其中 $f(0,j,k)=0$&lt;/p&gt;
&lt;p&gt;考虑特殊情况，如果 $op2=0$，那么就按照 $nums[i]$ 这个数进行集合划分，分为需不需要在 $nums[i]$ 上进行操作 1，因此 $f(i, j, 0) = min{f(i, j,0), f(i-1,j,0), f(i-1,j-1,0)-nums[i]/2}$&lt;/p&gt;
&lt;p&gt;如果 $op1=0$，同样也是按照 $nums[i]$ 这个数进行集合划分，分为需不需要在 $nums[i]$ 上进行操作 2，当然也需要满足题目的限制条件:
$$
f(i,0,j) = min{f(i,0,j), f(i-1,0,j), f(i-1,0,j-1)-k^\prime} \ , nums[i] \ge k^\prime
$$&lt;/p&gt;
&lt;p&gt;一般情况下，也就是 $op1&amp;gt;0,op2&amp;gt;0$，同样也是按照 $nums[i]$ 进行集合划分，此时可以同时对 $nums[i]$ 进行操作 1 和 操作 2（如果满足条件的话）。 首先考虑单独进行操作 1 和操作2，然后再进行操作 1 和操作 2 的组合。如果 $nums[i] \ge 2k^\prime-1$，那么一定是先应用操作 1 再应用操作 2，如果 $k^\prime\le nums[i] \lt 2k^\prime-1$，则必须先应用操作 2 再应用操作 1。因此，递推式如下
$$
f(i,j,k)=
min
\begin{cases}
f(i,j,k) , \
f(i, j-1,k) - nums[i]/2, \
f(i, j,k-1) - k^\prime,  \ \ if \ nums[i] \ge k^\prime\
f(i,i-1,j-1) - nums[i]/2-k^\prime, \ \ if \ nums[i] \ge 2*k^\prime-1 \
f(i,i-1,j-1)-k-(nums[i]-k)/2, \ \ if \  k^\prime\le nums[i] \lt 2k^\prime-1
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;java代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    final int INF = 0x3f3f3f3f;
    int[][][] f;
    public int minArraySum(int[] nums, int kt, int op1, int op2) {

        int n = nums.length;
        f = new int[n + 1][op1 + 1][op2 + 1];

        int t = 0;
        for (int i = 0; i &amp;lt; n; i ++ ) {
            t += nums[i];
            for (int x = 0; x &amp;lt;= op1; x ++ )
                for (int y = 0; y &amp;lt;= op2; y ++ )
                    f[i + 1][x][y] = t;
        }
        for (int i = 1; i &amp;lt;= n; i ++ ) {
            int v = nums[i - 1];
            for (int j = 1; j &amp;lt;= op1; j ++ )
                f[i][j][0] = Math.min(f[i][j][0], Math.min(f[i - 1][j][0], f[i - 1][j - 1][0] - v / 2) + v);
            for (int k = 1; k &amp;lt;= op2; k ++ ) {
                f[i][0][k] = Math.min(f[i][0][k], f[i-1][0][k] + v);
                if (v &amp;gt;= kt) f[i][0][k] = Math.min(f[i][0][k], f[i - 1][0][k - 1] - kt + v);
            }
            for (int j = 1; j &amp;lt;= op1; j ++ )
                for (int k = 1; k &amp;lt;= op2; k ++ ) {
                    f[i][j][k] = Math.min(f[i][j][k], Math.min(f[i - 1][j][k], f[i - 1][j - 1][k] - v / 2) + v);
                    if (v &amp;gt;= kt) f[i][j][k] = Math.min(f[i][j][k], f[i - 1][j][k - 1] - kt + v);
                    if (v &amp;gt;= 2 * kt - 1) f[i][j][k] = Math.min(f[i][j][k], f[i - 1][j - 1][k - 1] - v / 2 - kt + v);
                    if (v &amp;gt;= kt &amp;amp;&amp;amp; v &amp;lt; 2 * kt - 1) f[i][j][k] = Math.min(f[i][j][k], f[i - 1][j - 1][k - 1] - kt - (v - kt) / 2 + v);
                }
            
        }
        return f[n][op1][op2];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第 425 场周赛第四题&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;题目链接：https://leetcode.cn/problems/maximize-sum-of-weights-after-edge-removals/description/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;给定一棵 $n$ 个结点的无向树，结点编号是 $0$~$n-1$ ，给定一个数组 edges，其中 $edges[i]=[u_i, v_i, w_i]$，代表结点 $u_i,v_i$ 之间有一条边。&lt;/p&gt;
&lt;p&gt;现在需要你移除零条边或者多条边，使得&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个结点与至多 $k$ 个其他结点有边直接相连，其中 $k$ 是给定的输入&lt;/li&gt;
&lt;li&gt;剩余边的权重之和的最大化&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;求剩余边的权重的 &lt;strong&gt;最大&lt;/strong&gt; 可能和&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$2 \le n \le 10^5$&lt;/li&gt;
&lt;li&gt;$1 \le k \le n - 1$&lt;/li&gt;
&lt;li&gt;$edges.length = n-1$&lt;/li&gt;
&lt;li&gt;$edges[i] = 3$&lt;/li&gt;
&lt;li&gt;$0 \le edges[i][0] \le n - 1$&lt;/li&gt;
&lt;li&gt;$0 \le edges[i][1] \le n - 1$&lt;/li&gt;
&lt;li&gt;$1 \le edges[i][2] \le 10^6$&lt;/li&gt;
&lt;li&gt;保证&lt;code&gt;edges&lt;/code&gt;形成一棵有效的树&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;树形 dp, 贪心&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度$O(nlogn)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;题目有说这是一棵无根树，一般情况下会从一个结点开始，让其作为根节点，这里以 0 为根节点。&lt;/p&gt;
&lt;p&gt;然后树上的问题很容易找到子问题，我们仔细思考，对于一个以 $r$ 为根结点的树，如果该结点满足题目条件，那么与 $r$ 相关的边肯定不会删除，那么就只需要考虑其孩子结点即可，而以孩子结点为根的子树不影响 $r$ 为根的树，因此，可以考虑使用树形dp来求解。&lt;/p&gt;
&lt;p&gt;以 $f(i,0/1)$ 表示以 $i$ 结点为根的子树并且其唯一入度边的状态为 $0/1$（0表示不删除，1表示删除）的最大价值。因此答案为 $f(0,0)$，根节点为 0，根节点没有入度边，因此表示不删除即可。&lt;/p&gt;
&lt;p&gt;如果根节点的入度边删除，那么表示其出度边最多只能有 $k-1$ 条。&lt;/p&gt;
&lt;p&gt;因此，无非是需要保留的边减少了，这里我们忽略这些细节，就考虑以 $r$ 为根，并且出度边至多只能保留 $k$ 条的最大价值，$f(r,0),f(r,1)$ 的求法是一样的。&lt;/p&gt;
&lt;p&gt;首先递归求出 $f(r_1, 0),f(r_2,0),\cdots,f(r_x,0)$, 以及 $f(r_1,1),f(r_2,1),\cdots,f(r_x,1)$&lt;/p&gt;
&lt;p&gt;首先，如果 $f(r_y,0) \le f(r_y,1)$ ，换句话说删了 $(r,r_y)$ 这条边比不删好，那何乐而不为，直接删掉，也就是说此时累加 $f(r_y,1)$ 即可&lt;/p&gt;
&lt;p&gt;因此，接下来只需要考虑 $f(r_y,0) \gt f(r_y,1)$ 的情况，看下面的 note 便可求解。&lt;/p&gt;
&lt;p&gt;:::note
这里我抽象出一个问题：
给你一个长度为 $n$ 的两个数组，分别为 $A,B$，其中 $A[i] \gt B[i]$，
现在让你至多选 $k$ 个 $A$ 数组里面的数（注意，选了 $A[i]$ 就不能选 $B[i]$)，然后其余选 $B$ 数组里面的数，怎么选累和最大？
答：因为这里 $A[i] \gt B[i]$，因此最多选 $k$ 个，那就是选 $k$，除非 $n \lt k$。
假设选了 $k$ 个，分别为 $A[1],\cdots,A[k]$，其余选的 $B[k+1],\cdots,B[n]$ 。如果此时从选择的 $k$ 个中挑出 $A[x]$，从其余部分挑出 $B[y]$ ，将其交换到不同集合中去，我们看看累和的变化：
$$
\Delta=A[1]+\cdots+A[k]+B[k]+\cdots+B[n]- \ (A[1]+\cdots+A[y]+\cdots+A[k]+B[k+1]+\cdots+B[x]+\cdots+B[n]) = \
A[x] - A[y] + B[y] - B[x]
$$
如果要使得无论怎么交换 $\Delta$ 的值都是减少的，也就是说当 $\Delta\gt 0$ 时，也就是 $A[x]-B[x] \gt A[y] - B[y]$ 时，一定是最优解。
因此，如果我们按照 $A[x]-B[x]$ 进行排序，只需要选前 $k$ 个数为 A，后面的为 B, 则一定累和最大。
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JAVA代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Solution {
    int[] h, e, ne, w;
    long[][] f;
    int idx = 0;
    class Node implements Comparable&amp;lt;Node&amp;gt; {
        long x, y; // x 是包含的 y 是不包含的
        @Override
        public int compareTo(Node o) {
            if (x - y == o.x - o.y) return 0;
            return x - y - (o.x - o.y) &amp;gt; 0 ? -1 : 1;
        }
    }

    void add(int a, int b, int c) {
        e[idx] = b;
        w[idx] = c;
        ne[idx] = h[a];
        h[a] = idx ++ ;
    }
    void init(int n) {
        h = new int[n];
        e = new int[n &amp;lt;&amp;lt; 1];
        ne = new int[n &amp;lt;&amp;lt; 1];
        w = new int[n &amp;lt;&amp;lt; 1];
        Arrays.fill(h, -1);
        f = new long[n][2];
        for (int i = 0; i &amp;lt; n; i ++ ) f[i][0] = f[i][1] = -1;
    }
    long dp(int u, int fa, int isIn, int k) {
       if (f[u][isIn] != -1) return f[u][isIn];
        int cnt = 0;
        for (int i = h[u]; i != -1; i = ne[i]) {
            int v = e[i];
            if (v == fa) continue;
            f[v][0] = dp(v, u, 0, k);
            f[v][1] = dp(v, u, 1, k);
           // System.out.println(u + &quot; &quot; + v + &quot; &quot; + w[i]);

            cnt ++ ;
        }
        Node[] tmp = new Node[cnt]; // 必须是局部变量
        for (int i = h[u], j = 0; i != -1; i = ne[i]) {
            int v = e[i];
            if (v == fa) continue;
            tmp[j] = new Node();
            tmp[j].y = f[v][0];
            tmp[j].x = f[v][1] + w[i];
            j ++ ;
        }
        Arrays.sort(tmp, 0, cnt);
        long ans = 0;
        for (int i = 0; i &amp;lt; cnt; i ++ )
            if (i &amp;lt; k - isIn &amp;amp;&amp;amp; tmp[i].x &amp;gt; tmp[i].y) ans += tmp[i].x;
            else ans += tmp[i].y;
        return f[u][isIn] = ans;
    }
    public long maximizeSumOfWeights(int[][] edges, int k) {
        int n = edges.length; 
        init(n + 1);
        for (int[] e: edges) {
            add(e[0], e[1], e[2]);
            add(e[1], e[0], e[2]);
        }
        return dp(0, -1, 0, k);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
这几道题中，按照这四道题的顺序，第一、四题是比较苦难的，首先第一个涉及到了贪心，可能不好想，第二个细节比较多。&lt;/p&gt;
&lt;p&gt;另外第三题还有一个贪心解，这里暂时不做探究。
:::&lt;/p&gt;
</content:encoded></item><item><title>mybatis 必知必解</title><link>https://lzleureka.cn/posts/engineering/mybatis/mybatisnote/</link><guid isPermaLink="true">https://lzleureka.cn/posts/engineering/mybatis/mybatisnote/</guid><description>mybatis的各种知识点</description><pubDate>Fri, 25 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;关于mybatis的各种配置信息以及语法之类的，也包含了部分需要的日志、测试单元的写法&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Mybatis简介&lt;/h1&gt;
&lt;h2&gt;MyBatis历史&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下，iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;iBatis一词来源于“internet”和“abatis”的组合，是一个基于Java的持久层框架。iBatis提供的持久层框架包括SQL Maps和Data Access Objects（DAO）&lt;/p&gt;
&lt;p&gt;sql maps 是数据库数据与java数据的映射，也是封装jdbc的过程&lt;/p&gt;
&lt;p&gt;DAO：通过mybatis实现&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MyBatis特性&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架&lt;/p&gt;
&lt;p&gt;高级映射：满足一对多，多对一的情形&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集&lt;/p&gt;
&lt;p&gt;不用写jdbc代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyBatis可以使用简单的XML或注解用于配置和原始映射，将接口和Java的POJO（Plain Old Java Objects，普通的Java对象）映射成数据库中的记录&lt;/p&gt;
&lt;p&gt;sql语句在xml或者注解中写vb&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MyBatis 是一个 半自动的ORM（Object Relation Mapping）框架&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;​	jdbc，纯手动；mybatis，需要自己写sql；hiberlate，全自动，sql是生成。ORM，关系对象映射。对象$\leftrightarrow$ 数据&lt;/p&gt;
&lt;h2&gt;MyBatis下载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mybatis/mybatis-3&quot;&gt;MyBatis下载地址&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;Resources/MyBatis%E4%B8%8B%E8%BD%BD.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;和其它持久化层技术对比&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;JDBC
&lt;ul&gt;
&lt;li&gt;SQL 夹杂在Java代码中耦合度高，导致硬编码内伤  (代码写死了)&lt;/li&gt;
&lt;li&gt;维护不易且实际开发需求中 SQL 有变化，频繁修改的情况多见&lt;/li&gt;
&lt;li&gt;代码冗长，开发效率低&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Hibernate 和 JPA
&lt;ul&gt;
&lt;li&gt;操作简便，开发效率高&lt;/li&gt;
&lt;li&gt;程序中的长难复杂 SQL 需要绕过框架&lt;/li&gt;
&lt;li&gt;内部自动生产的 SQL，不容易做特殊优化&lt;/li&gt;
&lt;li&gt;基于全映射的全自动框架，大量字段的 POJO 进行部分映射时比较困难。&lt;/li&gt;
&lt;li&gt;反射操作太多，导致数据库性能下降&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MyBatis
&lt;ul&gt;
&lt;li&gt;轻量级，性能出色&lt;/li&gt;
&lt;li&gt;SQL 和 Java 编码分开，功能边界清晰。Java代码专注业务、SQL语句专注数据&lt;/li&gt;
&lt;li&gt;开发效率稍逊于HIbernate，但是完全能够接受&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;搭建MyBatis&lt;/h1&gt;
&lt;h2&gt;开发环境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;IDE：idea 2019.2&lt;/li&gt;
&lt;li&gt;构建工具：maven 3.5.4&lt;/li&gt;
&lt;li&gt;MySQL版本：MySQL 5.7&lt;/li&gt;
&lt;li&gt;MyBatis版本：MyBatis 3.5.7&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建maven工程&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;打包方式：jar&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;引入依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
	&amp;lt;!-- Mybatis核心 --&amp;gt;
	&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;org.mybatis&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;mybatis&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;3.5.7&amp;lt;/version&amp;gt;
	&amp;lt;/dependency&amp;gt;
	&amp;lt;!-- junit测试 --&amp;gt;
	&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;4.12&amp;lt;/version&amp;gt;
		&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
	&amp;lt;/dependency&amp;gt;
	&amp;lt;!-- MySQL驱动 --&amp;gt;
	&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;5.1.3&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建MyBatis的核心配置文件&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;习惯上命名为&lt;code&gt;mybatis-config.xml&lt;/code&gt;，这个文件名仅仅只是建议，并非强制要求。将来整合Spring之后，这个配置文件可以省略，所以大家操作时可以直接复制、粘贴。
核心配置文件主要用于配置连接数据库的环境以及MyBatis的全局配置信息
核心配置文件存放的位置是src/main/resources目录下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;  
&amp;lt;!DOCTYPE configuration  
PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;  
&quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&amp;gt;  
&amp;lt;configuration&amp;gt;  
	&amp;lt;!--设置连接数据库的环境--&amp;gt;  
	&amp;lt;environments default=&quot;development&quot;&amp;gt;  
		&amp;lt;environment id=&quot;development&quot;&amp;gt;  
			&amp;lt;transactionManager type=&quot;JDBC&quot;/&amp;gt;  
			&amp;lt;dataSource type=&quot;POOLED&quot;&amp;gt;  
				&amp;lt;property name=&quot;driver&quot; value=&quot;com.mysql.cj.jdbc.Driver&quot;/&amp;gt;  
				&amp;lt;property name=&quot;url&quot; value=&quot;jdbc:mysql://localhost:3306/MyBatis&quot;/&amp;gt;  
				&amp;lt;property name=&quot;username&quot; value=&quot;root&quot;/&amp;gt;  
				&amp;lt;property name=&quot;password&quot; value=&quot;123456&quot;/&amp;gt;  
			&amp;lt;/dataSource&amp;gt;  
		&amp;lt;/environment&amp;gt;  
	&amp;lt;/environments&amp;gt;  
	&amp;lt;!--引入映射文件--&amp;gt;  
	&amp;lt;mappers&amp;gt;  
		&amp;lt;mapper resource=&quot;mappers/UserMapper.xml&quot;/&amp;gt;  
	&amp;lt;/mappers&amp;gt;  
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建mapper接口&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;MyBatis中的mapper接口相当于以前的dao。但是区别在于，mapper仅仅是接口，我们不需要提供实现类&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;package com.atguigu.mybatis.mapper;  
  
public interface UserMapper {  
	/**  
	* 添加用户信息  
	*/  
	int insertUser();  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建MyBatis的映射文件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;相关概念：ORM（Object Relationship Mapping）对象关系映射。&lt;/li&gt;
&lt;li&gt;对象：Java的实体类对象
&lt;ul&gt;
&lt;li&gt;关系：关系型数据库&lt;/li&gt;
&lt;li&gt;映射：二者之间的对应关系&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Java概念&lt;/th&gt;
&lt;th&gt;数据库概念&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;类&lt;/td&gt;
&lt;td&gt;表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;属性&lt;/td&gt;
&lt;td&gt;字段/列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对象&lt;/td&gt;
&lt;td&gt;记录/行&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;映射文件的命名规则&lt;/li&gt;
&lt;li&gt;表所对应的实体类的类名+Mapper.xml
&lt;ul&gt;
&lt;li&gt;例如：表t_user，映射的实体类为User，所对应的映射文件为UserMapper.xml&lt;/li&gt;
&lt;li&gt;因此一个映射文件对应一个实体类，对应一张表的操作&lt;/li&gt;
&lt;li&gt;MyBatis映射文件用于编写SQL，访问以及操作表中的数据&lt;/li&gt;
&lt;li&gt;MyBatis映射文件存放的位置是src/main/resources/mappers目录下&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MyBatis中可以面向接口操作数据，要保证两个一致&lt;/li&gt;
&lt;li&gt;mapper接口的全类名和映射文件的命名空间（namespace）保持一致
&lt;ul&gt;
&lt;li&gt;mapper接口中方法的方法名和映射文件中编写SQL的标签的id属性保持一致&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;  
&amp;lt;!DOCTYPE mapper  
PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;  
&quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;  
&amp;lt;mapper namespace=&quot;com.atguigu.mybatis.mapper.UserMapper&quot;&amp;gt;  
	&amp;lt;!--int insertUser();--&amp;gt;  
	&amp;lt;insert id=&quot;insertUser&quot;&amp;gt;  
		insert into t_user values(null,&apos;张三&apos;,&apos;123&apos;,23,&apos;女&apos;)  
	&amp;lt;/insert&amp;gt;  
&amp;lt;/mapper&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过junit测试功能&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SqlSession：代表Java程序和数据库之间的会话。（HttpSession是Java程序和浏览器之间的会话）&lt;/li&gt;
&lt;li&gt;SqlSessionFactory：是“生产”SqlSession的“工厂”&lt;/li&gt;
&lt;li&gt;工厂模式：如果创建某一个对象，使用的过程基本固定，那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中，以后都使用这个工厂类来“生产”我们需要的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UserMapperTest {
    @Test
    public void testInsertUser() throws IOException {
        //读取MyBatis的核心配置文件
        InputStream is = Resources.getResourceAsStream(&quot;mybatis-config.xml&quot;);
        //获取SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
        //通过核心配置文件所对应的字节输入流创建工厂类SqlSessionFactory，生产SqlSession对象
        SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
        //获取sqlSession，此时通过SqlSession对象所操作的sql都必须手动提交或回滚事务
        //SqlSession sqlSession = sqlSessionFactory.openSession();
	    //创建SqlSession对象，此时通过SqlSession对象所操作的sql都会自动提交  
		SqlSession sqlSession = sqlSessionFactory.openSession(true);
        //通过代理模式创建UserMapper接口的代理实现类对象
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        //调用UserMapper接口中的方法，就可以根据UserMapper的全类名匹配元素文件，通过调用的方法名匹配映射文件中的SQL标签，并执行标签中的SQL语句
        int result = userMapper.insertUser();
        //提交事务
        //sqlSession.commit();
        System.out.println(&quot;result:&quot; + result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;此时需要手动提交事务，如果要自动提交事务，则在获取sqlSession对象时，使用&lt;code&gt;SqlSession sqlSession = sqlSessionFactory.openSession(true);&lt;/code&gt;，传入一个Boolean类型的参数，值为true，这样就可以自动提交&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;加入log4j日志功能&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;加入依赖&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- log4j日志 --&amp;gt;
&amp;lt;dependency&amp;gt;
&amp;lt;groupId&amp;gt;log4j&amp;lt;/groupId&amp;gt;
&amp;lt;artifactId&amp;gt;log4j&amp;lt;/artifactId&amp;gt;
&amp;lt;version&amp;gt;1.2.17&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加入log4j的配置文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;log4j的配置文件名为log4j.xml，存放的位置是src/main/resources目录下&lt;/li&gt;
&lt;li&gt;日志的级别：FATAL(致命)&amp;gt;ERROR(错误)&amp;gt;WARN(警告)&amp;gt;INFO(信息)&amp;gt;DEBUG(调试) 从左到右打印的内容越来越详细&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE log4j:configuration SYSTEM &quot;log4j.dtd&quot;&amp;gt;
&amp;lt;log4j:configuration xmlns:log4j=&quot;http://jakarta.apache.org/log4j/&quot;&amp;gt;
    &amp;lt;appender name=&quot;STDOUT&quot; class=&quot;org.apache.log4j.ConsoleAppender&quot;&amp;gt;
        &amp;lt;param name=&quot;Encoding&quot; value=&quot;UTF-8&quot; /&amp;gt;
        &amp;lt;layout class=&quot;org.apache.log4j.PatternLayout&quot;&amp;gt;
			&amp;lt;param name=&quot;ConversionPattern&quot; value=&quot;%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \n&quot; /&amp;gt;
        &amp;lt;/layout&amp;gt;
    &amp;lt;/appender&amp;gt;
    &amp;lt;logger name=&quot;java.sql&quot;&amp;gt;
        &amp;lt;level value=&quot;debug&quot; /&amp;gt;
    &amp;lt;/logger&amp;gt;
    &amp;lt;logger name=&quot;org.apache.ibatis&quot;&amp;gt;
        &amp;lt;level value=&quot;info&quot; /&amp;gt;
    &amp;lt;/logger&amp;gt;
    &amp;lt;root&amp;gt;
        &amp;lt;level value=&quot;debug&quot; /&amp;gt;
        &amp;lt;appender-ref ref=&quot;STDOUT&quot; /&amp;gt;
    &amp;lt;/root&amp;gt;
&amp;lt;/log4j:configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;核心配置文件详解&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;核心配置文件中的标签必须按照固定的顺序(有的标签可以不写，但顺序一定不能乱)：
properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE configuration
        PUBLIC &quot;-//MyBatis.org//DTD Config 3.0//EN&quot;
        &quot;http://MyBatis.org/dtd/MyBatis-3-config.dtd&quot;&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;!--引入properties文件，此时就可以${属性名}的方式访问属性值--&amp;gt;
    &amp;lt;properties resource=&quot;jdbc.properties&quot;&amp;gt;&amp;lt;/properties&amp;gt;
    &amp;lt;settings&amp;gt;
        &amp;lt;!--将表中字段的下划线自动转换为驼峰--&amp;gt;
        &amp;lt;setting name=&quot;mapUnderscoreToCamelCase&quot; value=&quot;true&quot;/&amp;gt;
        &amp;lt;!--开启延迟加载--&amp;gt;
        &amp;lt;setting name=&quot;lazyLoadingEnabled&quot; value=&quot;true&quot;/&amp;gt;
    &amp;lt;/settings&amp;gt;
    &amp;lt;typeAliases&amp;gt;
        &amp;lt;!--
        typeAlias：设置某个具体的类型的别名
        属性：
        type：需要设置别名的类型的全 类名
        alias：设置此类型的别名，且别名不区分大小写。若不设置此属性，该类型拥有默认的别名，即类名
        --&amp;gt;
        &amp;lt;!--&amp;lt;typeAlias type=&quot;com.atguigu.mybatis.bean.User&quot;&amp;gt;&amp;lt;/typeAlias&amp;gt;--&amp;gt;
        &amp;lt;!--&amp;lt;typeAlias type=&quot;com.atguigu.mybatis.bean.User&quot; alias=&quot;user&quot;&amp;gt;
        &amp;lt;/typeAlias&amp;gt;--&amp;gt;
        &amp;lt;!--以包为单位，设置改包下所有的类型都拥有默认的别名，即类名且不区分大小写--&amp;gt;
        &amp;lt;package name=&quot;com.atguigu.mybatis.bean&quot;/&amp;gt;
    &amp;lt;/typeAliases&amp;gt;
    &amp;lt;!--
    environments：设置多个连接数据库的环境
    属性：
	    default：设置默认使用的环境的id
    --&amp;gt;
    &amp;lt;environments default=&quot;mysql_test&quot;&amp;gt;
        &amp;lt;!--
        environment：设置具体的连接数据库的环境信息
        属性：
	        id：设置环境的唯一标识，可通过environments标签中的default设置某一个环境的id，表示默认使用的环境 
        --&amp;gt;
        &amp;lt;environment id=&quot;mysql_test&quot;&amp;gt;
            &amp;lt;!--
            transactionManager：设置事务管理方式
            属性：
	            type：设置事务管理方式，type=&quot;JDBC|MANAGED&quot;
	            type=&quot;JDBC&quot;：设置当前环境的事务管理都必须手动处理（提交或回滚）
	            type=&quot;MANAGED&quot;：设置事务被管理，例如spring中的AOP
            --&amp;gt;
            &amp;lt;transactionManager type=&quot;JDBC&quot;/&amp;gt;
            &amp;lt;!--
            dataSource：设置数据源
            属性：
	            type：设置数据源的类型，type=&quot;POOLED|UNPOOLED|JNDI&quot;
	            type=&quot;POOLED&quot;：使用数据库连接池，即会将创建的连接进行缓存，下次使用可以从缓存中直接获取，不需要重新创建
	            type=&quot;UNPOOLED&quot;：不使用数据库连接池，即每次使用连接都需要重新创建
	            type=&quot;JNDI&quot;：调用上下文中的数据源
            --&amp;gt;
            &amp;lt;dataSource type=&quot;POOLED&quot;&amp;gt;
                &amp;lt;!--设置驱动类的全类名--&amp;gt;
                &amp;lt;property name=&quot;driver&quot; value=&quot;${jdbc.driver}&quot;/&amp;gt;
                &amp;lt;!--设置连接数据库的连接地址--&amp;gt;
                &amp;lt;property name=&quot;url&quot; value=&quot;${jdbc.url}&quot;/&amp;gt;
                &amp;lt;!--设置连接数据库的用户名--&amp;gt;
                &amp;lt;property name=&quot;username&quot; value=&quot;${jdbc.username}&quot;/&amp;gt;
                &amp;lt;!--设置连接数据库的密码--&amp;gt;
                &amp;lt;property name=&quot;password&quot; value=&quot;${jdbc.password}&quot;/&amp;gt;
            &amp;lt;/dataSource&amp;gt;
        &amp;lt;/environment&amp;gt;
    &amp;lt;/environments&amp;gt;
    &amp;lt;!--引入映射文件--&amp;gt;
    &amp;lt;mappers&amp;gt;
        &amp;lt;!-- &amp;lt;mapper resource=&quot;UserMapper.xml&quot;/&amp;gt; --&amp;gt;
        &amp;lt;!--
        以包为单位，将包下所有的映射文件引入核心配置文件
        注意：
			1. 此方式必须保证mapper接口和mapper映射文件必须在相同的包下
			2. mapper接口要和mapper映射文件的名字一致
        --&amp;gt;
        &amp;lt;package name=&quot;com.atguigu.mybatis.mapper&quot;/&amp;gt;
    &amp;lt;/mappers&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;Resources/mapper%E6%8E%A5%E5%8F%A3%E5%92%8Cmapper%E6%98%A0%E5%B0%84%E6%96%87%E4%BB%B6%E5%9C%A8%E5%90%8C%E4%B8%80%E5%8C%85%E4%B8%8B.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;默认的类型别名&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;Resources/%E9%BB%98%E8%AE%A4%E7%9A%84%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D1.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;Resources/%E9%BB%98%E8%AE%A4%E7%9A%84%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;MyBatis的增删改查&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;添加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--int insertUser();--&amp;gt;
&amp;lt;insert id=&quot;insertUser&quot;&amp;gt;
	insert into t_user values(null,&apos;admin&apos;,&apos;123456&apos;,23,&apos;男&apos;,&apos;12345@qq.com&apos;)
&amp;lt;/insert&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--int deleteUser();--&amp;gt;
 &amp;lt;delete id=&quot;deleteUser&quot;&amp;gt;
     delete from t_user where id = 6
 &amp;lt;/delete&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--int updateUser();--&amp;gt;
 &amp;lt;update id=&quot;updateUser&quot;&amp;gt;
     update t_user set username = &apos;张三&apos; where id = 5
 &amp;lt;/update&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询一个实体类对象&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User getUserById();--&amp;gt;  
&amp;lt;select id=&quot;getUserById&quot; resultType=&quot;com.atguigu.mybatis.bean.User&quot;&amp;gt;  
	select * from t_user where id = 2  
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查询集合&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;User&amp;gt; getUserList();--&amp;gt;
&amp;lt;select id=&quot;getUserList&quot; resultType=&quot;com.atguigu.mybatis.bean.User&quot;&amp;gt;
	select * from t_user
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查询的标签select必须设置属性resultType或resultMap，用于设置实体类和数据库表的映射关系
&lt;ul&gt;
&lt;li&gt;resultType：自动映射，用于属性名和表中字段名一致的情况&lt;/li&gt;
&lt;li&gt;resultMap：自定义映射，用于一对多或多对一或字段名和属性名不一致的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当查询的数据为多条时，不能使用实体类作为返回值，只能使用集合，否则会抛出异常TooManyResultsException；但是若查询的数据只有一条，可以使用实体类或集合作为返回值&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;MyBatis获取参数值的两种方式（重点）&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;MyBatis获取参数值的两种方式：${}和#{}&lt;/li&gt;
&lt;li&gt;${}的本质就是字符串拼接，#{}的本质就是占位符赋值&lt;/li&gt;
&lt;li&gt;${}使用字符串拼接的方式拼接sql，若为字符串类型或日期类型的字段进行赋值时，需要手动加单引号；但是#{}使用占位符赋值的方式拼接sql，此时为字符串类型或日期类型的字段进行赋值时，可以自动添加单引号&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;单个字面量类型的参数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;若mapper接口中的方法参数为单个的字面量类型，此时可以使用${}和#{}以任意的名称（最好见名识意）获取参数的值，注意${}需要手动加单引号&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User getUserByUsername(String username);--&amp;gt;
&amp;lt;select id=&quot;getUserByUsername&quot; resultType=&quot;User&quot;&amp;gt;
	select * from t_user where username = #{username}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User getUserByUsername(String username);--&amp;gt;
&amp;lt;select id=&quot;getUserByUsername&quot; resultType=&quot;User&quot;&amp;gt;  
	select * from t_user where username = &apos;${username}&apos;  
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多个字面量类型的参数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若mapper接口中的方法参数为多个时，此时MyBatis会自动将这些参数放在一个map集合中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以arg0,arg1...为键，以参数为值；&lt;/li&gt;
&lt;li&gt;以param1,param2...为键，以参数为值；&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因此只需要通过${}和#{}访问map集合的键就可以获取相对应的值，注意${}需要手动加单引号。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用arg或者param都行，要注意的是，arg是从arg0开始的，param是从param1开始的&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User checkLogin(String username,String password);--&amp;gt;
&amp;lt;select id=&quot;checkLogin&quot; resultType=&quot;User&quot;&amp;gt;  
	select * from t_user where username = #{arg0} and password = #{arg1}  
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User checkLogin(String username,String password);--&amp;gt;
&amp;lt;select id=&quot;checkLogin&quot; resultType=&quot;User&quot;&amp;gt;
	select * from t_user where username = &apos;${param1}&apos; and password = &apos;${param2}&apos;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;map集合类型的参数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;若mapper接口中的方法需要的参数为多个时，此时可以手动创建map集合，将这些数据放在map中只需要通过${}和#{}访问map集合的键就可以获取相对应的值，注意${}需要手动加单引号&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User checkLoginByMap(Map&amp;lt;String,Object&amp;gt; map);--&amp;gt;
&amp;lt;select id=&quot;checkLoginByMap&quot; resultType=&quot;User&quot;&amp;gt;
	select * from t_user where username = #{username} and password = #{password}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void checkLoginByMap() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
	Map&amp;lt;String,Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
	map.put(&quot;usermane&quot;,&quot;admin&quot;);
	map.put(&quot;password&quot;,&quot;123456&quot;);
	User user = mapper.checkLoginByMap(map);
	System.out.println(user);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;实体类类型的参数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;若mapper接口中的方法参数为实体类对象时此时可以使用${}和#{}，通过访问实体类对象中的属性名获取属性值，注意${}需要手动加单引号&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--int insertUser(User user);--&amp;gt;
&amp;lt;insert id=&quot;insertUser&quot;&amp;gt;
	insert into t_user values(null,#{username},#{password},#{age},#{sex},#{email})
&amp;lt;/insert&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void insertUser() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
	User user = new User(null,&quot;Tom&quot;,&quot;123456&quot;,12,&quot;男&quot;,&quot;123@321.com&quot;);
	mapper.insertUser(user);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用@Param标识参数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;可以通过@Param注解标识mapper接口中的方法参数，此时，会将这些参数放在map集合中&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以@Param注解的value属性值为键，以参数为值；&lt;/li&gt;
&lt;li&gt;以param1,param2...为键，以参数为值；&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;只需要通过${}和#{}访问map集合的键就可以获取相对应的值，注意${}需要手动加单引号&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User CheckLoginByParam(@Param(&quot;username&quot;) String username, @Param(&quot;password&quot;) String password);--&amp;gt;
    &amp;lt;select id=&quot;CheckLoginByParam&quot; resultType=&quot;User&quot;&amp;gt;
        select * from t_user where username = #{username} and password = #{password}
    &amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void checkLoginByParam() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
	mapper.CheckLoginByParam(&quot;admin&quot;,&quot;123456&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;建议分成两种情况进行处理&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;实体类类型的参数&lt;/li&gt;
&lt;li&gt;使用@Param标识参数&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;MyBatis的各种查询功能&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;如果查询出的数据只有一条，可以通过
&lt;ol&gt;
&lt;li&gt;实体类对象接收&lt;/li&gt;
&lt;li&gt;List集合接收&lt;/li&gt;
&lt;li&gt;Map集合接收，结果&lt;code&gt;{password=123456, sex=男, id=1, age=23, username=admin}&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;如果查询出的数据有多条，一定不能用实体类对象接收，会抛异常TooManyResultsException，可以通过
&lt;ol&gt;
&lt;li&gt;实体类类型的LIst集合接收&lt;/li&gt;
&lt;li&gt;Map类型的LIst集合接收&lt;/li&gt;
&lt;li&gt;在mapper接口的方法上添加@MapKey注解&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;查询一个实体类对象&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据用户id查询用户信息
 * @param id
 * @return
 */
User getUserById(@Param(&quot;id&quot;) int id);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--User getUserById(@Param(&quot;id&quot;) int id);--&amp;gt;
&amp;lt;select id=&quot;getUserById&quot; resultType=&quot;User&quot;&amp;gt;
	select * from t_user where id = #{id}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询一个List集合&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 查询所有用户信息
 * @return
 */
List&amp;lt;User&amp;gt; getUserList();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;User&amp;gt; getUserList();--&amp;gt;
&amp;lt;select id=&quot;getUserList&quot; resultType=&quot;User&quot;&amp;gt;
	select * from t_user
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询单个数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**  
 * 查询用户的总记录数  
 * @return  
 * 在MyBatis中，对于Java中常用的类型都设置了类型别名  
 * 例如：java.lang.Integer--&amp;gt;int|integer  
 * 例如：int--&amp;gt;_int|_integer  
 * 例如：Map--&amp;gt;map,List--&amp;gt;list  
 */  
int getCount();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--int getCount();--&amp;gt;
&amp;lt;select id=&quot;getCount&quot; resultType=&quot;_integer&quot;&amp;gt;
	select count(id) from t_user
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询一条数据为map集合&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**  
 * 根据用户id查询用户信息为map集合  
 * @param id  
 * @return  
 */  
Map&amp;lt;String, Object&amp;gt; getUserToMap(@Param(&quot;id&quot;) int id);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--Map&amp;lt;String, Object&amp;gt; getUserToMap(@Param(&quot;id&quot;) int id);--&amp;gt;
&amp;lt;select id=&quot;getUserToMap&quot; resultType=&quot;map&quot;&amp;gt;
	select * from t_user where id = #{id}
&amp;lt;/select&amp;gt;
&amp;lt;!--结果：{password=123456, sex=男, id=1, age=23, username=admin}--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;查询多条数据为map集合&lt;/h2&gt;
&lt;h3&gt;方法一&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**  
 * 查询所有用户信息为map集合  
 * @return  
 * 将表中的数据以map集合的方式查询，一条数据对应一个map；若有多条数据，就会产生多个map集合，此时可以将这些map放在一个list集合中获取  
 */  
List&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; getAllUserToMap();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--Map&amp;lt;String, Object&amp;gt; getAllUserToMap();--&amp;gt;  
&amp;lt;select id=&quot;getAllUserToMap&quot; resultType=&quot;map&quot;&amp;gt;  
	select * from t_user  
&amp;lt;/select&amp;gt;
&amp;lt;!--
	结果：
	[{password=123456, sex=男, id=1, age=23, username=admin},
	{password=123456, sex=男, id=2, age=23, username=张三},
	{password=123456, sex=男, id=3, age=23, username=张三}]
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法二&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 查询所有用户信息为map集合
 * @return
 * 将表中的数据以map集合的方式查询，一条数据对应一个map；若有多条数据，就会产生多个map集合，并且最终要以一个map的方式返回数据，此时需要通过@MapKey注解设置map集合的键，值是每条数据所对应的map集合
 */
@MapKey(&quot;id&quot;)
Map&amp;lt;String, Object&amp;gt; getAllUserToMap();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--Map&amp;lt;String, Object&amp;gt; getAllUserToMap();--&amp;gt;
&amp;lt;select id=&quot;getAllUserToMap&quot; resultType=&quot;map&quot;&amp;gt;
	select * from t_user
&amp;lt;/select&amp;gt;
&amp;lt;!--
	结果：
	{
	1={password=123456, sex=男, id=1, age=23, username=admin},
	2={password=123456, sex=男, id=2, age=23, username=张三},
	3={password=123456, sex=男, id=3, age=23, username=张三}
	}
--&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;特殊SQL的执行&lt;/h1&gt;
&lt;h2&gt;模糊查询&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据用户名进行模糊查询
 * @param username 
 * @return java.util.List&amp;lt;com.atguigu.mybatis.pojo.User&amp;gt;
 * @date 2022/2/26 21:56
 */
List&amp;lt;User&amp;gt; getUserByLike(@Param(&quot;username&quot;) String username);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;User&amp;gt; getUserByLike(@Param(&quot;username&quot;) String username);--&amp;gt;
&amp;lt;select id=&quot;getUserByLike&quot; resultType=&quot;User&quot;&amp;gt;
	&amp;lt;!--select * from t_user where username like &apos;%${mohu}%&apos;--&amp;gt;  
	&amp;lt;!--select * from t_user where username like concat(&apos;%&apos;,#{mohu},&apos;%&apos;)--&amp;gt;  
	select * from t_user where username like &quot;%&quot;#{mohu}&quot;%&quot;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;其中&lt;code&gt;select * from t_user where username like &quot;%&quot;#{mohu}&quot;%&quot;&lt;/code&gt;是最常用的&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;批量删除&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;只能使用${}，如果使用#{}，则解析后的sql语句为&lt;code&gt;delete from t_user where id in (&apos;1,2,3&apos;)&lt;/code&gt;，这样是将&lt;code&gt;1,2,3&lt;/code&gt;看做是一个整体，只有id为&lt;code&gt;1,2,3&lt;/code&gt;的数据会被删除。正确的语句应该是&lt;code&gt;delete from t_user where id in (1,2,3)&lt;/code&gt;，或者&lt;code&gt;delete from t_user where id in (&apos;1&apos;,&apos;2&apos;,&apos;3&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 根据id批量删除
 * @param ids 
 * @return int
 * @date 2022/2/26 22:06
 */
int deleteMore(@Param(&quot;ids&quot;) String ids);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;delete id=&quot;deleteMore&quot;&amp;gt;
	delete from t_user where id in (${ids})
&amp;lt;/delete&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//测试类
@Test
public void deleteMore() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	SQLMapper mapper = sqlSession.getMapper(SQLMapper.class);
	int result = mapper.deleteMore(&quot;1,2,3,8&quot;);
	System.out.println(result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动态设置表名&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;只能使用${}，因为表名不能加单引号&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 查询指定表中的数据
 * @param tableName 
 * @return java.util.List&amp;lt;com.atguigu.mybatis.pojo.User&amp;gt;
 * @date 2022/2/27 14:41
 */
List&amp;lt;User&amp;gt; getUserByTable(@Param(&quot;tableName&quot;) String tableName);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;User&amp;gt; getUserByTable(@Param(&quot;tableName&quot;) String tableName);--&amp;gt;
&amp;lt;select id=&quot;getUserByTable&quot; resultType=&quot;User&quot;&amp;gt;
	select * from ${tableName}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;添加功能获取自增的主键&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用场景&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;t_clazz(clazz_id,clazz_name)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;t_student(student_id,student_name,clazz_id)&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;添加班级信息&lt;/li&gt;
&lt;li&gt;获取新添加的班级的id&lt;/li&gt;
&lt;li&gt;为班级分配学生，即将某学的班级id修改为新添加的班级的id&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在mapper.xml中设置两个属性&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;useGeneratedKeys：设置使用自增的主键&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keyProperty：因为增删改有统一的返回值是受影响的行数，因此只能将获取的自增的主键放在传输的参数user对象的某个属性中&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 添加用户信息
 * @param user 
 * @date 2022/2/27 15:04
 */
void insertUser(User user);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--void insertUser(User user);--&amp;gt;
&amp;lt;insert id=&quot;insertUser&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;id&quot;&amp;gt;
	insert into t_user values (null,#{username},#{password},#{age},#{sex},#{email})
&amp;lt;/insert&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//测试类
@Test
public void insertUser() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	SQLMapper mapper = sqlSession.getMapper(SQLMapper.class);
	User user = new User(null, &quot;ton&quot;, &quot;123&quot;, 23, &quot;男&quot;, &quot;123@321.com&quot;);
	mapper.insertUser(user);
	System.out.println(user);
	//输出：user{id=10, username=&apos;ton&apos;, password=&apos;123&apos;, age=23, sex=&apos;男&apos;, email=&apos;123@321.com&apos;}，自增主键存放到了user的id属性中
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;自定义映射resultMap&lt;/h1&gt;
&lt;h2&gt;resultMap处理字段和属性的映射关系&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;resultMap：设置自定义映射&lt;/li&gt;
&lt;li&gt;属性：
&lt;ul&gt;
&lt;li&gt;id：表示自定义映射的唯一标识，不能重复&lt;/li&gt;
&lt;li&gt;type：查询的数据要映射的实体类的类型&lt;/li&gt;
&lt;li&gt;子标签：&lt;/li&gt;
&lt;li&gt;id：设置主键的映射关系
&lt;ul&gt;
&lt;li&gt;result：设置普通字段的映射关系&lt;/li&gt;
&lt;li&gt;子标签属性：&lt;/li&gt;
&lt;li&gt;property：设置映射关系中实体类中的属性名
&lt;ul&gt;
&lt;li&gt;column：设置映射关系中表中的字段名&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;若字段名和实体类中的属性名不一致，则可以通过resultMap设置自定义映射，即使字段名和属性名一致的属性也要映射，也就是全部属性都要列出来&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;empResultMap&quot; type=&quot;Emp&quot;&amp;gt;
	&amp;lt;id property=&quot;eid&quot; column=&quot;eid&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;empName&quot; column=&quot;emp_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;age&quot; column=&quot;age&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;sex&quot; column=&quot;sex&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;email&quot; column=&quot;email&quot;&amp;gt;&amp;lt;/result&amp;gt;
&amp;lt;/resultMap&amp;gt;
&amp;lt;!--List&amp;lt;Emp&amp;gt; getAllEmp();--&amp;gt;
&amp;lt;select id=&quot;getAllEmp&quot; resultMap=&quot;empResultMap&quot;&amp;gt;
	select * from t_emp
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;若字段名和实体类中的属性名不一致，但是字段名符合数据库的规则（使用_），实体类中的属性名符合Java的规则（使用驼峰）。此时也可通过以下两种方式处理字段名和实体类中的属性的映射关系&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;可以通过为字段起别名的方式，保证和实体类中的属性名保持一致&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;Emp&amp;gt; getAllEmp();--&amp;gt;
&amp;lt;select id=&quot;getAllEmp&quot; resultType=&quot;Emp&quot;&amp;gt;
	select eid,emp_name empName,age,sex,email from t_emp
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;可以在MyBatis的核心配置文件中的&lt;code&gt;setting&lt;/code&gt;标签中，设置一个全局配置信息mapUnderscoreToCamelCase，可以在查询表中数据时，自动将_类型的字段名转换为驼峰，例如：字段名user_name，设置了mapUnderscoreToCamelCase，此时字段名就会转换为userName。&lt;a href=&quot;#%E6%A0%B8%E5%BF%83%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%A6%E8%A7%A3&quot;&gt;核心配置文件详解&lt;/a&gt;&lt;pre&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;settings&amp;gt;
&amp;lt;setting name=&quot;mapUnderscoreToCamelCase&quot; value=&quot;true&quot;/&amp;gt;
&amp;lt;/settings&amp;gt;
```&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;多对一映射处理&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;查询员工信息以及员工所对应的部门信息&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;public class Emp {  
	private Integer eid;  
	private String empName;  
	private Integer age;  
	private String sex;  
	private String email;  
	private Dept dept;
	//...构造器、get、set方法等
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;级联方式处理映射关系&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;empAndDeptResultMapOne&quot; type=&quot;Emp&quot;&amp;gt;
	&amp;lt;id property=&quot;eid&quot; column=&quot;eid&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;empName&quot; column=&quot;emp_name&quot;&amp;gt;&amp;lt;/result &amp;gt;
	&amp;lt;result property=&quot;age&quot; column=&quot;age&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;sex&quot; column=&quot;sex&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;email&quot; column=&quot;email&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;dept.did&quot; column=&quot;did&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;dept.deptName&quot; column=&quot;dept_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
&amp;lt;/resultMap&amp;gt;
&amp;lt;!--Emp getEmpAndDept(@Param(&quot;eid&quot;)Integer eid);--&amp;gt;
&amp;lt;select id=&quot;getEmpAndDept&quot; resultMap=&quot;empAndDeptResultMapOne&quot;&amp;gt;
	select * from t_emp left join t_dept on t_emp.eid = t_dept.did where t_emp.eid = #{eid}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用association处理映射关系&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;association：处理多对一的映射关系&lt;/li&gt;
&lt;li&gt;property：需要处理多对的映射关系的属性名&lt;/li&gt;
&lt;li&gt;javaType：该属性的类型&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;empAndDeptResultMapTwo&quot; type=&quot;Emp&quot;&amp;gt;
	&amp;lt;id property=&quot;eid&quot; column=&quot;eid&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;empName&quot; column=&quot;emp_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;age&quot; column=&quot;age&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;sex&quot; column=&quot;sex&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;email&quot; column=&quot;email&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;association property=&quot;dept&quot; javaType=&quot;Dept&quot;&amp;gt;
		&amp;lt;id property=&quot;did&quot; column=&quot;did&quot;&amp;gt;&amp;lt;/id&amp;gt;
		&amp;lt;result property=&quot;deptName&quot; column=&quot;dept_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;/association&amp;gt;
&amp;lt;/resultMap&amp;gt;
&amp;lt;!--Emp getEmpAndDept(@Param(&quot;eid&quot;)Integer eid);--&amp;gt;
&amp;lt;select id=&quot;getEmpAndDept&quot; resultMap=&quot;empAndDeptResultMapTwo&quot;&amp;gt;
	select * from t_emp left join t_dept on t_emp.eid = t_dept.did where t_emp.eid = #{eid}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分步查询&lt;/h3&gt;
&lt;h4&gt;1. 查询员工信息&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;select：设置分布查询的sql的唯一标识（namespace.SQLId或mapper接口的全类名.方法名）&lt;/li&gt;
&lt;li&gt;column：设置分步查询的条件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//EmpMapper里的方法
/**
 * 通过分步查询，员工及所对应的部门信息
 * 分步查询第一步：查询员工信息
 * @param  
 * @return com.atguigu.mybatis.pojo.Emp
 * @date 2022/2/27 20:17
 */
Emp getEmpAndDeptByStepOne(@Param(&quot;eid&quot;) Integer eid);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;empAndDeptByStepResultMap&quot; type=&quot;Emp&quot;&amp;gt;
	&amp;lt;id property=&quot;eid&quot; column=&quot;eid&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;empName&quot; column=&quot;emp_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;age&quot; column=&quot;age&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;sex&quot; column=&quot;sex&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;email&quot; column=&quot;email&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;association property=&quot;dept&quot;
				 select=&quot;com.atguigu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo&quot;
				 column=&quot;did&quot;&amp;gt;&amp;lt;/association&amp;gt;
&amp;lt;/resultMap&amp;gt;
&amp;lt;!--Emp getEmpAndDeptByStepOne(@Param(&quot;eid&quot;) Integer eid);--&amp;gt;
&amp;lt;select id=&quot;getEmpAndDeptByStepOne&quot; resultMap=&quot;empAndDeptByStepResultMap&quot;&amp;gt;
	select * from t_emp where eid = #{eid}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 查询部门信息&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;//DeptMapper里的方法
/**
 * 通过分步查询，员工及所对应的部门信息
 * 分步查询第二步：通过did查询员工对应的部门信息
 * @param
 * @return com.atguigu.mybatis.pojo.Emp
 * @date 2022/2/27 20:23
 */
Dept getEmpAndDeptByStepTwo(@Param(&quot;did&quot;) Integer did);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--此处的resultMap仅是处理字段和属性的映射关系--&amp;gt;
&amp;lt;resultMap id=&quot;EmpAndDeptByStepTwoResultMap&quot; type=&quot;Dept&quot;&amp;gt;
	&amp;lt;id property=&quot;did&quot; column=&quot;did&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;deptName&quot; column=&quot;dept_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
&amp;lt;/resultMap&amp;gt;
&amp;lt;!--Dept getEmpAndDeptByStepTwo(@Param(&quot;did&quot;) Integer did);--&amp;gt;
&amp;lt;select id=&quot;getEmpAndDeptByStepTwo&quot; resultMap=&quot;EmpAndDeptByStepTwoResultMap&quot;&amp;gt;
	select * from t_dept where did = #{did}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;一对多映射处理&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;public class Dept {
    private Integer did;
    private String deptName;
    private List&amp;lt;Emp&amp;gt; emps;
	//...构造器、get、set方法等
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;collection&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;collection：用来处理一对多的映射关系&lt;/li&gt;
&lt;li&gt;ofType：表示该属性对饮的集合中存储的数据的类型&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;DeptAndEmpResultMap&quot; type=&quot;Dept&quot;&amp;gt;
	&amp;lt;id property=&quot;did&quot; column=&quot;did&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;deptName&quot; column=&quot;dept_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;collection property=&quot;emps&quot; ofType=&quot;Emp&quot;&amp;gt;
		&amp;lt;id property=&quot;eid&quot; column=&quot;eid&quot;&amp;gt;&amp;lt;/id&amp;gt;
		&amp;lt;result property=&quot;empName&quot; column=&quot;emp_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
		&amp;lt;result property=&quot;age&quot; column=&quot;age&quot;&amp;gt;&amp;lt;/result&amp;gt;
		&amp;lt;result property=&quot;sex&quot; column=&quot;sex&quot;&amp;gt;&amp;lt;/result&amp;gt;
		&amp;lt;result property=&quot;email&quot; column=&quot;email&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;/collection&amp;gt;
&amp;lt;/resultMap&amp;gt;
&amp;lt;!--Dept getDeptAndEmp(@Param(&quot;did&quot;) Integer did);--&amp;gt;
&amp;lt;select id=&quot;getDeptAndEmp&quot; resultMap=&quot;DeptAndEmpResultMap&quot;&amp;gt;
	select * from t_dept left join t_emp on t_dept.did = t_emp.did where t_dept.did = #{did}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分步查询&lt;/h3&gt;
&lt;h4&gt;1. 查询部门信息&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 通过分步查询，查询部门及对应的所有员工信息
 * 分步查询第一步：查询部门信息
 * @param did 
 * @return com.atguigu.mybatis.pojo.Dept
 * @date 2022/2/27 22:04
 */
Dept getDeptAndEmpByStepOne(@Param(&quot;did&quot;) Integer did);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;DeptAndEmpByStepOneResultMap&quot; type=&quot;Dept&quot;&amp;gt;
	&amp;lt;id property=&quot;did&quot; column=&quot;did&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;deptName&quot; column=&quot;dept_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;collection property=&quot;emps&quot;
				select=&quot;com.atguigu.mybatis.mapper.EmpMapper.getDeptAndEmpByStepTwo&quot;
				column=&quot;did&quot;&amp;gt;&amp;lt;/collection&amp;gt;
&amp;lt;/resultMap&amp;gt;
&amp;lt;!--Dept getDeptAndEmpByStepOne(@Param(&quot;did&quot;) Integer did);--&amp;gt;
&amp;lt;select id=&quot;getDeptAndEmpByStepOne&quot; resultMap=&quot;DeptAndEmpByStepOneResultMap&quot;&amp;gt;
	select * from t_dept where did = #{did}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 根据部门id查询部门中的所有员工&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 通过分步查询，查询部门及对应的所有员工信息
 * 分步查询第二步：根据部门id查询部门中的所有员工
 * @param did
 * @return java.util.List&amp;lt;com.atguigu.mybatis.pojo.Emp&amp;gt;
 * @date 2022/2/27 22:10
 */
List&amp;lt;Emp&amp;gt; getDeptAndEmpByStepTwo(@Param(&quot;did&quot;) Integer did);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;Emp&amp;gt; getDeptAndEmpByStepTwo(@Param(&quot;did&quot;) Integer did);--&amp;gt;
&amp;lt;select id=&quot;getDeptAndEmpByStepTwo&quot; resultType=&quot;Emp&quot;&amp;gt;
	select * from t_emp where did = #{did}
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;延迟加载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;分步查询的优点：可以实现延迟加载，但是必须在核心配置文件中设置全局配置信息：&lt;/li&gt;
&lt;li&gt;lazyLoadingEnabled：延迟加载的全局开关。当开启时，所有关联对象都会延迟加载
&lt;ul&gt;
&lt;li&gt;aggressiveLazyLoading：当开启时，任何方法的调用都会加载该对象的所有属性。 否则，每个属性会按需加载&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;此时就可以实现按需加载，获取的数据是什么，就只会执行相应的sql。此时可通过association和collection中的fetchType属性设置当前的分步查询是否使用延迟加载，fetchType=&quot;lazy(延迟加载)|eager(立即加载)&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;settings&amp;gt;
	&amp;lt;!--开启延迟加载--&amp;gt;
	&amp;lt;setting name=&quot;lazyLoadingEnabled&quot; value=&quot;true&quot;/&amp;gt;
&amp;lt;/settings&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void getEmpAndDeptByStepOne() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	Emp emp = mapper.getEmpAndDeptByStepOne(1);
	System.out.println(emp.getEmpName());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;关闭延迟加载，两条SQL语句都运行了&lt;img src=&quot;Resources/%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD%E6%B5%8B%E8%AF%951.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;开启延迟加载，只运行获取emp的SQL语句
&lt;img src=&quot;Resources/%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD%E6%B5%8B%E8%AF%952.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void getEmpAndDeptByStepOne() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	Emp emp = mapper.getEmpAndDeptByStepOne(1);
	System.out.println(emp.getEmpName());
	System.out.println(&quot;----------------&quot;);
	System.out.println(emp.getDept());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;开启后，需要用到查询dept的时候才会调用相应的SQL语句&lt;img src=&quot;Resources/%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD%E6%B5%8B%E8%AF%953.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;fetchType：当开启了全局的延迟加载之后，可以通过该属性手动控制延迟加载的效果，fetchType=&quot;lazy(延迟加载)|eager(立即加载)&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resultMap id=&quot;empAndDeptByStepResultMap&quot; type=&quot;Emp&quot;&amp;gt;
	&amp;lt;id property=&quot;eid&quot; column=&quot;eid&quot;&amp;gt;&amp;lt;/id&amp;gt;
	&amp;lt;result property=&quot;empName&quot; column=&quot;emp_name&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;age&quot; column=&quot;age&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;sex&quot; column=&quot;sex&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;result property=&quot;email&quot; column=&quot;email&quot;&amp;gt;&amp;lt;/result&amp;gt;
	&amp;lt;association property=&quot;dept&quot;
				 select=&quot;com.atguigu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo&quot;
				 column=&quot;did&quot;
				 fetchType=&quot;lazy&quot;&amp;gt;&amp;lt;/association&amp;gt;
&amp;lt;/resultMap&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;动态SQL&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Mybatis框架的动态SQL技术是一种根据特定条件动态拼装SQL语句的功能，它存在的意义是为了解决拼接SQL语句字符串时的痛点问题&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;if&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;if标签可通过test属性（即传递过来的数据）的表达式进行判断，若表达式的结果为true，则标签中的内容会执行；反之标签中的内容不会执行&lt;/li&gt;
&lt;li&gt;在where后面添加一个恒成立条件&lt;code&gt;1=1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;这个恒成立条件并不会影响查询的结果
&lt;ul&gt;
&lt;li&gt;这个&lt;code&gt;1=1&lt;/code&gt;可以用来拼接&lt;code&gt;and&lt;/code&gt;语句，例如：当empName为null时&lt;/li&gt;
&lt;li&gt;如果不加上恒成立条件，则SQL语句为&lt;code&gt;select * from t_emp where and age = ? and sex = ? and email = ?&lt;/code&gt;，此时&lt;code&gt;where&lt;/code&gt;会与&lt;code&gt;and&lt;/code&gt;连用，SQL语句会报错
&lt;ul&gt;
&lt;li&gt;如果加上一个恒成立条件，则SQL语句为&lt;code&gt;select * from t_emp where 1= 1 and age = ? and sex = ? and email = ?&lt;/code&gt;，此时不报错&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;Emp&amp;gt; getEmpByCondition(Emp emp);--&amp;gt;
&amp;lt;select id=&quot;getEmpByCondition&quot; resultType=&quot;Emp&quot;&amp;gt;
	select * from t_emp where 1=1
	&amp;lt;if test=&quot;empName != null and empName !=&apos;&apos;&quot;&amp;gt;
		and emp_name = #{empName}
	&amp;lt;/if&amp;gt;
	&amp;lt;if test=&quot;age != null and age !=&apos;&apos;&quot;&amp;gt;
		and age = #{age}
	&amp;lt;/if&amp;gt;
	&amp;lt;if test=&quot;sex != null and sex !=&apos;&apos;&quot;&amp;gt;
		and sex = #{sex}
	&amp;lt;/if&amp;gt;
	&amp;lt;if test=&quot;email != null and email !=&apos;&apos;&quot;&amp;gt;
		and email = #{email}
	&amp;lt;/if&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;where&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;where和if一般结合使用：&lt;/li&gt;
&lt;li&gt;若where标签中的if条件都不满足，则where标签没有任何功能，即不会添加where关键字
&lt;ul&gt;
&lt;li&gt;若where标签中的if条件满足，则where标签会自动添加where关键字，并将条件最前方多余的and/or去掉&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;Emp&amp;gt; getEmpByCondition(Emp emp);--&amp;gt;
&amp;lt;select id=&quot;getEmpByCondition&quot; resultType=&quot;Emp&quot;&amp;gt;
	select * from t_emp
	&amp;lt;where&amp;gt;
		&amp;lt;if test=&quot;empName != null and empName !=&apos;&apos;&quot;&amp;gt;
			emp_name = #{empName}
		&amp;lt;/if&amp;gt;
		&amp;lt;if test=&quot;age != null and age !=&apos;&apos;&quot;&amp;gt;
			and age = #{age}
		&amp;lt;/if&amp;gt;
		&amp;lt;if test=&quot;sex != null and sex !=&apos;&apos;&quot;&amp;gt;
			and sex = #{sex}
		&amp;lt;/if&amp;gt;
		&amp;lt;if test=&quot;email != null and email !=&apos;&apos;&quot;&amp;gt;
			and email = #{email}
		&amp;lt;/if&amp;gt;
	&amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;注意：where标签不能去掉条件后多余的and/or&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--这种用法是错误的，只能去掉条件前面的and/or，条件后面的不行--&amp;gt;
&amp;lt;if test=&quot;empName != null and empName !=&apos;&apos;&quot;&amp;gt;
emp_name = #{empName} and
&amp;lt;/if&amp;gt;
&amp;lt;if test=&quot;age != null and age !=&apos;&apos;&quot;&amp;gt;
	age = #{age}
&amp;lt;/if&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;trim&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;trim用于去掉或添加标签中的内容&lt;/li&gt;
&lt;li&gt;常用属性
&lt;ul&gt;
&lt;li&gt;prefix：在trim标签中的内容的前面添加某些内容&lt;/li&gt;
&lt;li&gt;suffix：在trim标签中的内容的后面添加某些内容&lt;/li&gt;
&lt;li&gt;prefixOverrides：在trim标签中的内容的前面去掉某些内容&lt;/li&gt;
&lt;li&gt;suffixOverrides：在trim标签中的内容的后面去掉某些内容&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;若trim中的标签都不满足条件，则trim标签没有任何效果，也就是只剩下&lt;code&gt;select * from t_emp&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;Emp&amp;gt; getEmpByCondition(Emp emp);--&amp;gt;
&amp;lt;select id=&quot;getEmpByCondition&quot; resultType=&quot;Emp&quot;&amp;gt;
	select * from t_emp
	&amp;lt;trim prefix=&quot;where&quot; suffixOverrides=&quot;and|or&quot;&amp;gt;
		&amp;lt;if test=&quot;empName != null and empName !=&apos;&apos;&quot;&amp;gt;
			emp_name = #{empName} and
		&amp;lt;/if&amp;gt;
		&amp;lt;if test=&quot;age != null and age !=&apos;&apos;&quot;&amp;gt;
			age = #{age} and
		&amp;lt;/if&amp;gt;
		&amp;lt;if test=&quot;sex != null and sex !=&apos;&apos;&quot;&amp;gt;
			sex = #{sex} or
		&amp;lt;/if&amp;gt;
		&amp;lt;if test=&quot;email != null and email !=&apos;&apos;&quot;&amp;gt;
			email = #{email}
		&amp;lt;/if&amp;gt;
	&amp;lt;/trim&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//测试类
@Test
public void getEmpByCondition() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
	List&amp;lt;Emp&amp;gt; emps= mapper.getEmpByCondition(new Emp(null, &quot;张三&quot;, null, null, null, null));
	System.out.println(emps);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;Resources/trim%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;choose、when、otherwise&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;choose、when、 otherwise&lt;/code&gt;相当于&lt;code&gt;if...else if..else&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;when至少要有一个，otherwise至多只有一个&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;select id=&quot;getEmpByChoose&quot; resultType=&quot;Emp&quot;&amp;gt;
	select * from t_emp
	&amp;lt;where&amp;gt;
		&amp;lt;choose&amp;gt;
			&amp;lt;when test=&quot;empName != null and empName != &apos;&apos;&quot;&amp;gt;
				emp_name = #{empName}
			&amp;lt;/when&amp;gt;
			&amp;lt;when test=&quot;age != null and age != &apos;&apos;&quot;&amp;gt;
				age = #{age}
			&amp;lt;/when&amp;gt;
			&amp;lt;when test=&quot;sex != null and sex != &apos;&apos;&quot;&amp;gt;
				sex = #{sex}
			&amp;lt;/when&amp;gt;
			&amp;lt;when test=&quot;email != null and email != &apos;&apos;&quot;&amp;gt;
				email = #{email}
			&amp;lt;/when&amp;gt;
			&amp;lt;otherwise&amp;gt;
				did = 1
			&amp;lt;/otherwise&amp;gt;
		&amp;lt;/choose&amp;gt;
	&amp;lt;/where&amp;gt;
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void getEmpByChoose() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
	List&amp;lt;Emp&amp;gt; emps = mapper.getEmpByChoose(new Emp(null, &quot;张三&quot;, 23, &quot;男&quot;, &quot;123@qq.com&quot;, null));
	System.out.println(emps);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;Resources/choose%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;相当于&lt;code&gt;if a else if b else if c else d&lt;/code&gt;，只会执行其中一个&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;foreach&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;属性：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;collection：设置要循环的数组或集合&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;item：表示集合或数组中的每一个数据&lt;/li&gt;
&lt;li&gt;separator：设置循环体之间的分隔符，分隔符前后默认有一个空格，如&lt;code&gt;,&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;open：设置foreach标签中的内容的开始符&lt;/li&gt;
&lt;li&gt;close：设置foreach标签中的内容的结束符&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;批量删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--int deleteMoreByArray(Integer[] eids);--&amp;gt;
&amp;lt;delete id=&quot;deleteMoreByArray&quot;&amp;gt;
	delete from t_emp where eid in
	&amp;lt;foreach collection=&quot;eids&quot; item=&quot;eid&quot; separator=&quot;,&quot; open=&quot;(&quot; close=&quot;)&quot;&amp;gt;
		#{eid}
	&amp;lt;/foreach&amp;gt;
&amp;lt;/delete&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void deleteMoreByArray() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
	int result = mapper.deleteMoreByArray(new Integer[]{6, 7, 8, 9});
	System.out.println(result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;Resources/foreach%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;批量添加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--int insertMoreByList(@Param(&quot;emps&quot;) List&amp;lt;Emp&amp;gt; emps);--&amp;gt;
&amp;lt;insert id=&quot;insertMoreByList&quot;&amp;gt;
	insert into t_emp values
	&amp;lt;foreach collection=&quot;emps&quot; item=&quot;emp&quot; separator=&quot;,&quot;&amp;gt;
		(null,#{emp.empName},#{emp.age},#{emp.sex},#{emp.email},null)
	&amp;lt;/foreach&amp;gt;
&amp;lt;/insert&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void insertMoreByList() {
	SqlSession sqlSession = SqlSessionUtils.getSqlSession();
	DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class);
	Emp emp1 = new Emp(null,&quot;a&quot;,1,&quot;男&quot;,&quot;123@321.com&quot;,null);
	Emp emp2 = new Emp(null,&quot;b&quot;,1,&quot;男&quot;,&quot;123@321.com&quot;,null);
	Emp emp3 = new Emp(null,&quot;c&quot;,1,&quot;男&quot;,&quot;123@321.com&quot;,null);
	List&amp;lt;Emp&amp;gt; emps = Arrays.asList(emp1, emp2, emp3);
	int result = mapper.insertMoreByList(emps);
	System.out.println(result);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;Resources/foreach%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SQL片段&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;sql片段，可以记录一段公共sql片段，在使用的地方通过include标签进行引入&lt;/li&gt;
&lt;li&gt;声明sql片段：&lt;code&gt;&amp;lt;sql&amp;gt;&lt;/code&gt;标签&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;sql id=&quot;empColumns&quot;&amp;gt;eid,emp_name,age,sex,email&amp;lt;/sql&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;引用sql片段：&lt;code&gt;&amp;lt;include&amp;gt;&lt;/code&gt;标签&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--List&amp;lt;Emp&amp;gt; getEmpByCondition(Emp emp);--&amp;gt;
&amp;lt;select id=&quot;getEmpByCondition&quot; resultType=&quot;Emp&quot;&amp;gt;
	select &amp;lt;include refid=&quot;empColumns&quot;&amp;gt;&amp;lt;/include&amp;gt; from t_emp
&amp;lt;/select&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;MyBatis的缓存&lt;/h1&gt;
&lt;h2&gt;MyBatis的一级缓存&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;一级缓存是SqlSession级别的，通过同一个SqlSession查询的数据会被缓存，下次查询相同的数据，就会从缓存中直接获取，不会从数据库重新访问&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使一级缓存失效的四种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不同的SqlSession对应不同的一级缓存&lt;/li&gt;
&lt;li&gt;同一个SqlSession但是查询条件不同&lt;/li&gt;
&lt;li&gt;同一个SqlSession两次查询期间执行了任何一次增删改操作&lt;/li&gt;
&lt;li&gt;同一个SqlSession两次查询期间手动清空了缓存&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MyBatis的二级缓存&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;二级缓存是SqlSessionFactory级别，通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存；此后若再次执行相同的查询语句，结果就会从缓存中获取&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;二级缓存开启的条件&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在核心配置文件中，设置全局配置属性cacheEnabled=&quot;true&quot;，默认为true，不需要设置&lt;/li&gt;
&lt;li&gt;在映射文件中设置标签&amp;lt;cache /&amp;gt;&lt;/li&gt;
&lt;li&gt;二级缓存必须在SqlSession关闭或提交之后有效&lt;/li&gt;
&lt;li&gt;查询的数据所转换的实体类类型必须实现序列化的接口&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使二级缓存失效的情况：两次查询之间执行了任意的增删改，会使一级和二级缓存同时失效&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二级缓存的相关配置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;在mapper配置文件中添加的cache标签可以设置一些属性&lt;/li&gt;
&lt;li&gt;eviction属性：缓存回收策略&lt;/li&gt;
&lt;li&gt;LRU（Least Recently Used） – 最近最少使用的：移除最长时间不被使用的对象。
&lt;ul&gt;
&lt;li&gt;FIFO（First in First out） – 先进先出：按对象进入缓存的顺序来移除它们。&lt;/li&gt;
&lt;li&gt;SOFT – 软引用：移除基于垃圾回收器状态和软引用规则的对象。&lt;/li&gt;
&lt;li&gt;WEAK – 弱引用：更积极地移除基于垃圾收集器状态和弱引用规则的对象。&lt;/li&gt;
&lt;li&gt;默认的是 LRU&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;flushInterval属性：刷新间隔，单位毫秒&lt;/li&gt;
&lt;li&gt;默认情况是不设置，也就是没有刷新间隔，缓存仅仅调用语句（增删改）时刷新&lt;/li&gt;
&lt;li&gt;size属性：引用数目，正整数&lt;/li&gt;
&lt;li&gt;代表缓存最多可以存储多少个对象，太大容易导致内存溢出&lt;/li&gt;
&lt;li&gt;readOnly属性：只读，true/false&lt;/li&gt;
&lt;li&gt;true：只读缓存；会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
&lt;ul&gt;
&lt;li&gt;false：读写缓存；会返回缓存对象的拷贝（通过序列化）。这会慢一些，但是安全，因此默认是false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;MyBatis缓存查询的顺序&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;先查询二级缓存，因为二级缓存中可能会有其他程序已经查出来的数据，可以拿来直接使用&lt;/li&gt;
&lt;li&gt;如果二级缓存没有命中，再查询一级缓存&lt;/li&gt;
&lt;li&gt;如果一级缓存也没有命中，则查询数据库&lt;/li&gt;
&lt;li&gt;SqlSession关闭之后，一级缓存中的数据会写入二级缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;整合第三方缓存EHCache（了解）&lt;/h2&gt;
&lt;h3&gt;添加依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Mybatis EHCache整合包 --&amp;gt;
&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;org.mybatis.caches&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;mybatis-ehcache&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;1.2.1&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;!-- slf4j日志门面的一个具体实现 --&amp;gt;
&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;ch.qos.logback&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;logback-classic&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;1.2.3&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;各个jar包的功能&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;jar包名称&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mybatis-ehcache&lt;/td&gt;
&lt;td&gt;Mybatis和EHCache的整合包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ehcache&lt;/td&gt;
&lt;td&gt;EHCache核心包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slf4j-api&lt;/td&gt;
&lt;td&gt;SLF4J日志门面包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;logback-classic&lt;/td&gt;
&lt;td&gt;支持SLF4J门面接口的一个具体实现&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;创建EHCache的配置文件ehcache.xml&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;名字必须叫&lt;code&gt;ehcache.xml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&amp;gt;
&amp;lt;ehcache xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:noNamespaceSchemaLocation=&quot;../config/ehcache.xsd&quot;&amp;gt;
    &amp;lt;!-- 磁盘保存路径 --&amp;gt;
    &amp;lt;diskStore path=&quot;D:\atguigu\ehcache&quot;/&amp;gt;
    &amp;lt;defaultCache
            maxElementsInMemory=&quot;1000&quot;
            maxElementsOnDisk=&quot;10000000&quot;
            eternal=&quot;false&quot;
            overflowToDisk=&quot;true&quot;
            timeToIdleSeconds=&quot;120&quot;
            timeToLiveSeconds=&quot;120&quot;
            diskExpiryThreadIntervalSeconds=&quot;120&quot;
            memoryStoreEvictionPolicy=&quot;LRU&quot;&amp;gt;
    &amp;lt;/defaultCache&amp;gt;
&amp;lt;/ehcache&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;设置二级缓存的类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在xxxMapper.xml文件中设置二级缓存类型&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;cache type=&quot;org.mybatis.caches.ehcache.EhcacheCache&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;加入logback日志&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;存在SLF4J时，作为简易日志的log4j将失效，此时我们需要借助SLF4J的具体实现logback来打印日志。创建logback的配置文件&lt;code&gt;logback.xml&lt;/code&gt;，名字固定，不可改变&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;configuration debug=&quot;true&quot;&amp;gt;
    &amp;lt;!-- 指定日志输出的位置 --&amp;gt;
    &amp;lt;appender name=&quot;STDOUT&quot;
              class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;!-- 日志输出的格式 --&amp;gt;
            &amp;lt;!-- 按照顺序分别是：时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 --&amp;gt;
            &amp;lt;pattern&amp;gt;[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;
    &amp;lt;!-- 设置全局日志级别。日志级别按顺序分别是：DEBUG、INFO、WARN、ERROR --&amp;gt;
    &amp;lt;!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 --&amp;gt;
    &amp;lt;root level=&quot;DEBUG&quot;&amp;gt;
        &amp;lt;!-- 指定打印日志的appender，这里通过“STDOUT”引用了前面配置的appender --&amp;gt;
        &amp;lt;appender-ref ref=&quot;STDOUT&quot; /&amp;gt;
    &amp;lt;/root&amp;gt;
    &amp;lt;!-- 根据特殊需求指定局部日志级别 --&amp;gt;
    &amp;lt;logger name=&quot;com.atguigu.crowd.mapper&quot; level=&quot;DEBUG&quot;/&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;EHCache配置文件说明&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;属性名&lt;/th&gt;
&lt;th&gt;是否必须&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;maxElementsInMemory&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在内存中缓存的element的最大数目&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxElementsOnDisk&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;在磁盘上缓存的element的最大数目，若是0表示无穷大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eternal&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;设定缓存的elements是否永远不过期。 如果为true，则缓存的数据始终有效， 如果为false那么还要根据timeToIdleSeconds、timeToLiveSeconds判断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;overflowToDisk&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;timeToIdleSeconds&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时， 这些数据便会删除，默认值是0,也就是可闲置时间无穷大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;timeToLiveSeconds&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;缓存element的有效生命期，默认是0.,也就是element存活时间无穷大&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;diskSpoolBufferSizeMB&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;diskPersistent&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;在VM重启的时候是否启用磁盘保存EhCache中的数据，默认是false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;diskExpiryThreadIntervalSeconds&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;磁盘缓存的清理线程运行间隔，默认是120秒。每个120s， 相应的线程会进行一次EhCache中数据的清理工作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;memoryStoreEvictionPolicy&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;当内存缓存达到最大，有新的element加入的时候， 移除缓存中element的策略。 默认是LRU（最近最少使用），可选的有LFU（最不常使用）和FIFO（先进先出&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;MyBatis的逆向工程&lt;/h1&gt;
&lt;p&gt;本质：代码生成器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正向工程：先创建Java实体类，由框架负责根据实体类生成数据库表。Hibernate是支持正向工程的&lt;/li&gt;
&lt;li&gt;逆向工程：先创建数据库表，由框架负责根据数据库表，反向生成如下资源：&lt;/li&gt;
&lt;li&gt;Java实体类
&lt;ul&gt;
&lt;li&gt;Mapper接口&lt;/li&gt;
&lt;li&gt;Mapper映射文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;创建逆向工程的步骤&lt;/h2&gt;
&lt;h3&gt;添加依赖和插件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependencies&amp;gt;
	&amp;lt;!-- MyBatis核心依赖包 --&amp;gt;
	&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;org.mybatis&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;mybatis&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;3.5.9&amp;lt;/version&amp;gt;
	&amp;lt;/dependency&amp;gt;
	&amp;lt;!-- junit测试 --&amp;gt;
	&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;junit&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;junit&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;4.13.2&amp;lt;/version&amp;gt;
		&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
	&amp;lt;/dependency&amp;gt;
	&amp;lt;!-- MySQL驱动 --&amp;gt;
	&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;8.0.27&amp;lt;/version&amp;gt;
	&amp;lt;/dependency&amp;gt;
	&amp;lt;!-- log4j日志 --&amp;gt;
	&amp;lt;dependency&amp;gt;
		&amp;lt;groupId&amp;gt;log4j&amp;lt;/groupId&amp;gt;
		&amp;lt;artifactId&amp;gt;log4j&amp;lt;/artifactId&amp;gt;
		&amp;lt;version&amp;gt;1.2.17&amp;lt;/version&amp;gt;
	&amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;
&amp;lt;!-- 控制Maven在构建过程中相关配置 --&amp;gt;
&amp;lt;build&amp;gt;
	&amp;lt;!-- 构建过程中用到的插件 --&amp;gt;
	&amp;lt;plugins&amp;gt;
		&amp;lt;!-- 具体插件，逆向工程的操作是以构建过程中插件形式出现的 --&amp;gt;
		&amp;lt;plugin&amp;gt;
			&amp;lt;groupId&amp;gt;org.mybatis.generator&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;mybatis-generator-maven-plugin&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;1.3.0&amp;lt;/version&amp;gt;
			&amp;lt;!-- 插件的依赖 --&amp;gt;
			&amp;lt;dependencies&amp;gt;
				&amp;lt;!-- 逆向工程的核心依赖 --&amp;gt;
				&amp;lt;dependency&amp;gt;
					&amp;lt;groupId&amp;gt;org.mybatis.generator&amp;lt;/groupId&amp;gt;
					&amp;lt;artifactId&amp;gt;mybatis-generator-core&amp;lt;/artifactId&amp;gt;
					&amp;lt;version&amp;gt;1.3.2&amp;lt;/version&amp;gt;
				&amp;lt;/dependency&amp;gt;
				&amp;lt;!-- 数据库连接池 --&amp;gt;
				&amp;lt;dependency&amp;gt;
					&amp;lt;groupId&amp;gt;com.mchange&amp;lt;/groupId&amp;gt;
					&amp;lt;artifactId&amp;gt;c3p0&amp;lt;/artifactId&amp;gt;
					&amp;lt;version&amp;gt;0.9.2&amp;lt;/version&amp;gt;
				&amp;lt;/dependency&amp;gt;
				&amp;lt;!-- MySQL驱动 --&amp;gt;
				&amp;lt;dependency&amp;gt;
					&amp;lt;groupId&amp;gt;mysql&amp;lt;/groupId&amp;gt;
					&amp;lt;artifactId&amp;gt;mysql-connector-java&amp;lt;/artifactId&amp;gt;
					&amp;lt;version&amp;gt;8.0.27&amp;lt;/version&amp;gt;
				&amp;lt;/dependency&amp;gt;
			&amp;lt;/dependencies&amp;gt;
		&amp;lt;/plugin&amp;gt;
	&amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建MyBatis的核心配置文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&amp;gt;
&amp;lt;!DOCTYPE configuration
        PUBLIC &quot;-//mybatis.org//DTD Config 3.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-3-config.dtd&quot;&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;properties resource=&quot;jdbc.properties&quot;/&amp;gt;
    &amp;lt;typeAliases&amp;gt;
        &amp;lt;package name=&quot;&quot;/&amp;gt;
    &amp;lt;/typeAliases&amp;gt;
    &amp;lt;environments default=&quot;development&quot;&amp;gt;
        &amp;lt;environment id=&quot;development&quot;&amp;gt;
            &amp;lt;transactionManager type=&quot;JDBC&quot;/&amp;gt;
            &amp;lt;dataSource type=&quot;POOLED&quot;&amp;gt;
                &amp;lt;property name=&quot;driver&quot; value=&quot;${jdbc.driver}&quot;/&amp;gt;
                &amp;lt;property name=&quot;url&quot; value=&quot;${jdbc.url}&quot;/&amp;gt;
                &amp;lt;property name=&quot;username&quot; value=&quot;${jdbc.username}&quot;/&amp;gt;
                &amp;lt;property name=&quot;password&quot; value=&quot;${jdbc.password}&quot;/&amp;gt;
            &amp;lt;/dataSource&amp;gt;
        &amp;lt;/environment&amp;gt;
    &amp;lt;/environments&amp;gt;
    &amp;lt;mappers&amp;gt;
        &amp;lt;package name=&quot;&quot;/&amp;gt;
    &amp;lt;/mappers&amp;gt;
&amp;lt;/configuration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建逆向工程的配置文件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;文件名必须是：&lt;code&gt;generatorConfig.xml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE generatorConfiguration
        PUBLIC &quot;-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN&quot;
        &quot;http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd&quot;&amp;gt;
&amp;lt;generatorConfiguration&amp;gt;
    &amp;lt;!--
    targetRuntime: 执行生成的逆向工程的版本
    MyBatis3Simple: 生成基本的CRUD（清新简洁版）
    MyBatis3: 生成带条件的CRUD（奢华尊享版）
    --&amp;gt;
    &amp;lt;context id=&quot;DB2Tables&quot; targetRuntime=&quot;MyBatis3Simple&quot;&amp;gt;
        &amp;lt;!-- 数据库的连接信息 --&amp;gt;
        &amp;lt;jdbcConnection driverClass=&quot;com.mysql.cj.jdbc.Driver&quot;
                        connectionURL=&quot;jdbc:mysql://localhost:3306/mybatis&quot;
                        userId=&quot;root&quot;
                        password=&quot;123456&quot;&amp;gt;
        &amp;lt;/jdbcConnection&amp;gt;
        &amp;lt;!-- javaBean的生成策略--&amp;gt;
        &amp;lt;javaModelGenerator targetPackage=&quot;com.atguigu.mybatis.pojo&quot; targetProject=&quot;.\src\main\java&quot;&amp;gt;
            &amp;lt;property name=&quot;enableSubPackages&quot; value=&quot;true&quot; /&amp;gt;
            &amp;lt;property name=&quot;trimStrings&quot; value=&quot;true&quot; /&amp;gt;
        &amp;lt;/javaModelGenerator&amp;gt;
        &amp;lt;!-- SQL映射文件的生成策略 --&amp;gt;
        &amp;lt;sqlMapGenerator targetPackage=&quot;com.atguigu.mybatis.mapper&quot;
                         targetProject=&quot;.\src\main\resources&quot;&amp;gt;
            &amp;lt;property name=&quot;enableSubPackages&quot; value=&quot;true&quot; /&amp;gt;
        &amp;lt;/sqlMapGenerator&amp;gt;
        &amp;lt;!-- Mapper接口的生成策略 --&amp;gt;
        &amp;lt;javaClientGenerator type=&quot;XMLMAPPER&quot;
                             targetPackage=&quot;com.atguigu.mybatis.mapper&quot; targetProject=&quot;.\src\main\java&quot;&amp;gt;
            &amp;lt;property name=&quot;enableSubPackages&quot; value=&quot;true&quot; /&amp;gt;
        &amp;lt;/javaClientGenerator&amp;gt;
        &amp;lt;!-- 逆向分析的表 --&amp;gt;
        &amp;lt;!-- tableName设置为*号，可以对应所有表，此时不写domainObjectName --&amp;gt;
        &amp;lt;!-- domainObjectName属性指定生成出来的实体类的类名 --&amp;gt;
        &amp;lt;table tableName=&quot;t_emp&quot; domainObjectName=&quot;Emp&quot;/&amp;gt;
        &amp;lt;table tableName=&quot;t_dept&quot; domainObjectName=&quot;Dept&quot;/&amp;gt;
    &amp;lt;/context&amp;gt;
&amp;lt;/generatorConfiguration&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;执行MBG插件的generate目标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;Resources/%E6%89%A7%E8%A1%8CMBG%E6%8F%92%E4%BB%B6%E7%9A%84generate%E7%9B%AE%E6%A0%87.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;如果出现报错：&lt;code&gt;Exception getting JDBC Driver&lt;/code&gt;，可能是pom.xml中，数据库驱动配置错误&lt;/li&gt;
&lt;li&gt;dependency中的驱动&lt;img src=&quot;Resources/dependency%E4%B8%AD%E7%9A%84%E9%A9%B1%E5%8A%A8.png&quot; alt=&quot;&quot; /&gt;
&lt;ul&gt;
&lt;li&gt;mybatis-generator-maven-plugin插件中的驱动&lt;img src=&quot;Resources/%E6%8F%92%E4%BB%B6%E4%B8%AD%E7%9A%84%E9%A9%B1%E5%8A%A8.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;两者的驱动版本应该相同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;执行结果&lt;img src=&quot;Resources/%E9%80%86%E5%90%91%E6%89%A7%E8%A1%8C%E7%BB%93%E6%9E%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;QBC&lt;/h2&gt;
&lt;h3&gt;查询&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;selectByExample&lt;/code&gt;：按条件查询，需要传入一个example对象或者null；如果传入一个null，则表示没有条件，也就是查询所有数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;example.createCriteria().xxx&lt;/code&gt;：创建条件对象，通过andXXX方法为SQL添加查询添加，每个条件之间是and关系&lt;/li&gt;
&lt;li&gt;&lt;code&gt;example.or().xxx&lt;/code&gt;：将之前添加的条件通过or拼接其他条件
&lt;img src=&quot;Resources/example%E7%9A%84%E6%96%B9%E6%B3%95.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Test public void testMBG() throws IOException {
	InputStream is = Resources.getResourceAsStream(&quot;mybatis-config.xml&quot;);
	SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
	SqlSession sqlSession = sqlSessionFactory.openSession(true);
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	EmpExample example = new EmpExample();
	//名字为张三，且年龄大于等于20
	example.createCriteria().andEmpNameEqualTo(&quot;张三&quot;).andAgeGreaterThanOrEqualTo(20);
	//或者did不为空
	example.or().andDidIsNotNull();
	List&amp;lt;Emp&amp;gt; emps = mapper.selectByExample(example);
	emps.forEach(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;Resources/example%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;增改&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;updateByPrimaryKey&lt;/code&gt;：通过主键进行数据修改，如果某一个值为null，也会将对应的字段改为null&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapper.updateByPrimaryKey(new Emp(1,&quot;admin&quot;,22,null,&quot;456@qq.com&quot;,3));&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;Resources/%E5%A2%9E%E5%88%A0%E6%94%B9%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C1.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updateByPrimaryKeySelective()&lt;/code&gt;：通过主键进行选择性数据修改，如果某个值为null，则不修改这个字段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mapper.updateByPrimaryKeySelective(new Emp(2,&quot;admin2&quot;,22,null,&quot;456@qq.com&quot;,3));&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src=&quot;Resources/%E5%A2%9E%E5%88%A0%E6%94%B9%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C2.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;分页插件&lt;/h1&gt;
&lt;h2&gt;分页插件使用步骤&lt;/h2&gt;
&lt;h3&gt;添加依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper --&amp;gt;
&amp;lt;dependency&amp;gt;
	&amp;lt;groupId&amp;gt;com.github.pagehelper&amp;lt;/groupId&amp;gt;
	&amp;lt;artifactId&amp;gt;pagehelper&amp;lt;/artifactId&amp;gt;
	&amp;lt;version&amp;gt;5.2.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置分页插件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在MyBatis的核心配置文件（mybatis-config.xml）中配置插件&lt;/li&gt;
&lt;li&gt;&lt;img src=&quot;Resources/%E9%85%8D%E7%BD%AE%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;plugins&amp;gt;
	&amp;lt;!--设置分页插件--&amp;gt;
	&amp;lt;plugin interceptor=&quot;com.github.pagehelper.PageInterceptor&quot;&amp;gt;&amp;lt;/plugin&amp;gt;
&amp;lt;/plugins&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;分页插件的使用&lt;/h2&gt;
&lt;h3&gt;开启分页功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在查询功能之前使用&lt;code&gt;PageHelper.startPage(int pageNum, int pageSize)&lt;/code&gt;开启分页功能&lt;/li&gt;
&lt;li&gt;pageNum：当前页的页码&lt;/li&gt;
&lt;li&gt;pageSize：每页显示的条数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testPageHelper() throws IOException {
	InputStream is = Resources.getResourceAsStream(&quot;mybatis-config.xml&quot;);
	SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
	SqlSession sqlSession = sqlSessionFactory.openSession(true);
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	//访问第一页，每页四条数据
	PageHelper.startPage(1,4);
	List&amp;lt;Emp&amp;gt; emps = mapper.selectByExample(null);
	emps.forEach(System.out::println);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;Resources/%E5%88%86%E9%A1%B5%E6%B5%8B%E8%AF%95%E7%BB%93%E6%9E%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;分页相关数据&lt;/h3&gt;
&lt;h4&gt;方法一：直接输出&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testPageHelper() throws IOException {
	InputStream is = Resources.getResourceAsStream(&quot;mybatis-config.xml&quot;);
	SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
	SqlSession sqlSession = sqlSessionFactory.openSession(true);
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	//访问第一页，每页四条数据
	Page&amp;lt;Object&amp;gt; page = PageHelper.startPage(1, 4);
	List&amp;lt;Emp&amp;gt; emps = mapper.selectByExample(null);
	//在查询到List集合后，打印分页数据
	System.out.println(page);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分页相关数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=8, pages=2, reasonable=false, pageSizeZero=false}[Emp{eid=1, empName=&apos;admin&apos;, age=22, sex=&apos;男&apos;, email=&apos;456@qq.com&apos;, did=3}, Emp{eid=2, empName=&apos;admin2&apos;, age=22, sex=&apos;男&apos;, email=&apos;456@qq.com&apos;, did=3}, Emp{eid=3, empName=&apos;王五&apos;, age=12, sex=&apos;女&apos;, email=&apos;123@qq.com&apos;, did=3}, Emp{eid=4, empName=&apos;赵六&apos;, age=32, sex=&apos;男&apos;, email=&apos;123@qq.com&apos;, did=1}]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;方法二使用PageInfo&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;在查询获取list集合之后，使用&lt;code&gt;PageInfo&amp;lt;T&amp;gt; pageInfo = new PageInfo&amp;lt;&amp;gt;(List&amp;lt;T&amp;gt; list, intnavigatePages)&lt;/code&gt;获取分页相关数据&lt;/li&gt;
&lt;li&gt;list：分页之后的数据
&lt;ul&gt;
&lt;li&gt;navigatePages：导航分页的页码数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Test
public void testPageHelper() throws IOException {
	InputStream is = Resources.getResourceAsStream(&quot;mybatis-config.xml&quot;);
	SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
	SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
	SqlSession sqlSession = sqlSessionFactory.openSession(true);
	EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
	PageHelper.startPage(1, 4);
	List&amp;lt;Emp&amp;gt; emps = mapper.selectByExample(null);
	PageInfo&amp;lt;Emp&amp;gt; page = new PageInfo&amp;lt;&amp;gt;(emps,5);
	System.out.println(page);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分页相关数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PageInfo{
pageNum=1, pageSize=4, size=4, startRow=1, endRow=4, total=8, pages=2, 
list=Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=8, pages=2, reasonable=false, pageSizeZero=false}[Emp{eid=1, empName=&apos;admin&apos;, age=22, sex=&apos;男&apos;, email=&apos;456@qq.com&apos;, did=3}, Emp{eid=2, empName=&apos;admin2&apos;, age=22, sex=&apos;男&apos;, email=&apos;456@qq.com&apos;, did=3}, Emp{eid=3, empName=&apos;王五&apos;, age=12, sex=&apos;女&apos;, email=&apos;123@qq.com&apos;, did=3}, Emp{eid=4, empName=&apos;赵六&apos;, age=32, sex=&apos;男&apos;, email=&apos;123@qq.com&apos;, did=1}], 
prePage=0, nextPage=2, isFirstPage=true, isLastPage=false, hasPreviousPage=false, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=2, navigatepageNums=[1, 2]}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;其中list中的数据等同于方法一中直接输出的page数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;常用数据：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;pageNum：当前页的页码&lt;/li&gt;
&lt;li&gt;pageSize：每页显示的条数&lt;/li&gt;
&lt;li&gt;size：当前页显示的真实条数&lt;/li&gt;
&lt;li&gt;total：总记录数&lt;/li&gt;
&lt;li&gt;pages：总页数&lt;/li&gt;
&lt;li&gt;prePage：上一页的页码&lt;/li&gt;
&lt;li&gt;nextPage：下一页的页码&lt;/li&gt;
&lt;li&gt;isFirstPage/isLastPage：是否为第一页/最后一页&lt;/li&gt;
&lt;li&gt;hasPreviousPage/hasNextPage：是否存在上一页/下一页&lt;/li&gt;
&lt;li&gt;navigatePages：导航分页的页码数&lt;/li&gt;
&lt;li&gt;navigatepageNums：导航分页的页码，[1,2,3,4,5]&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>云智研发笔试</title><link>https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E4%BA%91%E6%99%BA%E7%A0%94%E5%8F%91%E7%AC%94%E8%AF%95/</link><guid isPermaLink="true">https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E4%BA%91%E6%99%BA%E7%A0%94%E5%8F%91%E7%AC%94%E8%AF%95/</guid><description>云智研发笔试题解</description><pubDate>Wed, 04 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;由于笔试是全屏，所以只提供思路&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;第一题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;有 $t$ 个样例&lt;/p&gt;
&lt;p&gt;给你一个长度为 $n$ 的整数序列，问有多少对下标 $(i,j), i \lt j$ 满足 $a[i] + a[j] = a[i] \oplus a[j]$&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1 \le t \le 100$&lt;/li&gt;
&lt;li&gt;$1 \le n \le 10^3$&lt;/li&gt;
&lt;li&gt;$1 \le \sum n \le 10^3$&lt;/li&gt;
&lt;li&gt;$a_i \ge 1$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;枚举&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n^2)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;枚举 $(i,j)$ 二元组，满足条件便累加&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂无&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;第二题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;给你三个数，分别是 $n, m, k$ ，分别表示有 $n$ 个人，这 $n$ 个人的编号为 $1-n$，有 $m$ 个苹果，小明是编号是 $k$ 。&lt;/p&gt;
&lt;p&gt;现在我们需要分配苹果，每个人至少分配一个，并且要保证相邻的两个人分配的苹果相差不能大于 $1$，求所有分配方案中，小明能分配到的最大苹果数量。&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1 \le n \le 10^5$&lt;/li&gt;
&lt;li&gt;$1 \le n \le m \le 10^9$&lt;/li&gt;
&lt;li&gt;$1 \le k \le n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;二分&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(logm)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;假设我们知道小明拥有的苹果数，想要尽可能小明拥有的苹果数多，那么其他的的苹果数就尽可能少，同时需要满足相邻编号的人苹果数最多相差 $1$。&lt;/p&gt;
&lt;p&gt;根据贪心性质，小明最多，编号左右两边按 1 递减，就好像一个单峰（山峰）一样。比如，小明 k = 3, n = 4,  （1 2 3 2 1）的数据呈现是一个单峰&lt;/p&gt;
&lt;p&gt;另外要注意，如果递减的过程中发现会 $\le 0$，此时需要分配苹果数为 $1$ （满足最少分配一个苹果的条件）&lt;/p&gt;
&lt;p&gt;同时，也可以发现一个性质，如果小明分配的苹果越多，通过这种方式分配，可能苹果数不够，而小明分配的苹果越少，那么最后需要的苹果数可能是小于等于总苹果数的，如果是小于，我们把剩余的苹果从端点慢慢开始分配即可。比如：（1 2 3 2 1）剩余 4 -&amp;gt; (2, 2, 3, 2, 1) 剩余 3 -&amp;gt; …… -&amp;gt; (2, 3, 3, 3, 2) 剩余 0&lt;/p&gt;
&lt;p&gt;综上，得到一个结果，可以通过二分小明的苹果数量，范围为 $[1,m]$，然后计算一下通过贪心方式分配需要多少苹果，如果需要的苹果小于等于苹果总数，则舍弃左半区间，否则舍弃右半区间&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂无&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;第三题&lt;/h1&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;由于长度为 $n$ 的字符串，只包含小写字母，求有多少个连续子串，其包含 $r$ 和 $e$，但是不包含 $d$&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;$n \le 30000$&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;尺取&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;从前往后扫描，设 $p$ 记录第 $0$ 个位置（下标从 $1$ 开始），或者当前遍历的位置，左边第一个 $d$ 的下标。设 $r,e$ 分别记录从 $p + 1$ 开始 $r,e$ 字符分别出现的最后位置。&lt;/p&gt;
&lt;p&gt;如果当前 $r,e$ 字符全部遍历到了，并且没有遍历到 $d$，那么以当前下标为最后一个字符串的所有满足题意的连续字串数量为 $min(r,e) - p$&lt;/p&gt;
&lt;p&gt;如果当前遍历到了 $d$，那么更新一下 $p, r, e$ 的值，因为后面的满足题意的字符一定不会包含当前字符。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂无&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::tip
注意开 long
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;写在最后，题目不难，但是第二题第三题注意开 long&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>华为笔试</title><link>https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E5%8D%8E%E4%B8%BA%E7%AC%94%E8%AF%95/</link><guid isPermaLink="true">https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E5%8D%8E%E4%B8%BA%E7%AC%94%E8%AF%95/</guid><description>华为后端笔试题解</description><pubDate>Wed, 28 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;题比较简单。第一题：7min，第二题：13min，第三题：30min&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;第一题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;一行输入，输入是有 $n$ 个数字，每个数字按照空格隔开。注意：$n$ 在输入并未给出。&lt;/p&gt;
&lt;p&gt;然后他有个这样的题意：每个数字都是一只老鼠的编号，它们发现有一个地洞，那么这些老鼠就按照输入的数字编号钻到地洞去，然后如果碰到有重复的编号，说明这只老鼠就在以其它上面的老鼠都需要出来，然后当前编号的老鼠再跳进去，到最后所有的老鼠都要出来。问：老鼠出地洞序列&lt;/p&gt;
&lt;p&gt;比如：输入：1 2 3 1 3 3 1，则输出为 3 2 1 3 3 1 1。
解释：首先 1 2 3 老鼠入洞，然后碰到 1 ，说明按照 3 2 1 顺序出洞，然后老鼠 1 3 再入洞；碰到 3 ，按照 3 顺序出洞，然后老鼠 3 再入洞；碰到 1，按照 3 1 顺序出洞，老鼠 1 入洞。最后都要出洞，老鼠 1 再出洞。&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1 \le n \le 10^5$&lt;/li&gt;
&lt;li&gt;$1 \le A_i \le 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;栈&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其实按照题意已经知道是栈了，洞就是一个栈，满足先进后出的规则&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;

using namespace std;

const int N = 10010;

int n;
int q[N], stk[N], tt = 0;
bool in_stk[N];

int main() {

    // freopen(&quot;in.txt&quot;, &quot;r&quot;, stdin);
    // freopen(&quot;out.txt&quot;, &quot;w&quot;, stdout);
    
    int x; 
    int flag = 0;
    while (~scanf(&quot;%d&quot;, &amp;amp;x)) {
        if (in_stk[x]) {
            while (in_stk[x]) {
                int y = stk[-- tt];
                in_stk[y] = false;
                if (flag ++ ) putchar(&apos; &apos;);
                printf(&quot;%d&quot;, y);
            }
        } 
        stk[tt ++ ] = x;
        in_stk[x] = true;
    }

    while (tt &amp;gt; 0) {
        if (flag ++ ) putchar(&apos; &apos;);
        printf(&quot;%d&quot;, stk[-- tt]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第二题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;给你一个长度为 $n$ 的数组，然后一个大小为 $interval$ 的时间间隔。&lt;/p&gt;
&lt;p&gt;求：在数组里面任取一个数 $x$，然后有一个集合 ${x+ interval*k, k \ge 0}$ 的集合，那么说明数组里面有 $y$ 个数在这个集合里面，求最大的 $y$ 对应的 $x$，如果两个 $x$ 值的 $y$ 相等，则求最小的 $x$&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1 \le n \le 10^5$&lt;/li&gt;
&lt;li&gt;$0 \le x \le 10^9$&lt;/li&gt;
&lt;li&gt;$0 \le interval \le 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;思维&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(nlogn)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一步预处理 $h$ 数组，$h[i]$ 表示数组里面的数对 $interval$ 取模后有多少个值为 $i$&lt;/p&gt;
&lt;p&gt;第二步，有序离散化。因为对于重复值，只需要计算一次。同时使用哈希表记录下每个数出现了多少次&lt;/p&gt;
&lt;p&gt;第三步，从小到大枚举有序离散化后的数组，那么对于当前值 $x$， $y$ 一定是等于 $h[x % interval]$ 的数量。同时在最后 $h[x % interval]$ 要更新，减去原数组里面 $x$ 的数量。因为 $h[i]$ 只是保证了取模后的值为 $i$ 的数量，但是对于一个 $x$ 而言，我们需要的不仅仅是取模要相等，而且要保证 $\ge x$&lt;/p&gt;
&lt;p&gt;:::warning
因为这题 interval 可能为 0，因此需要特判
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;vector&amp;gt;

using namespace std;

const int N = 100010;

int q[N];
int n;
int delta;
int h[N];

unordered_map&amp;lt;int, int&amp;gt; um;
vector&amp;lt;int&amp;gt; alls;

int main() {

    // freopen(&quot;in.txt&quot;, &quot;r&quot;, stdin);
    // freopen(&quot;out.txt&quot;, &quot;w&quot;, stdout);
    scanf(&quot;%d&quot;, &amp;amp;n);
    for (int i = 0; i &amp;lt; n; i ++ ) {
        scanf(&quot;%d&quot;, &amp;amp;q[i]);
        alls.push_back(q[i]);
        um[q[i]] ++ ;
    }
    scanf(&quot;%d&quot;, &amp;amp;delta);

    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());

    int m = alls.size();

    if (delta == 0) {
        int res = 0, cnt = 0;
        for (auto &amp;amp;u: alls)
            if (um[u] &amp;gt; cnt) {
                res = u, cnt = um[u];
            }
        printf(&quot;%d&quot;, res);
    } else {
        for (int i = 0; i &amp;lt; n; i ++ )
            h[q[i] % delta] ++ ;
        
        int res = -1, cnt = 0;
        for (int i = 0; i &amp;lt; m; i ++ ) {
            int t = alls[i] % delta;
            if (h[t] &amp;gt; cnt) {
                res = alls[i], cnt = h[t];
            }
            h[t] -= um[alls[i]];
        }
        printf(&quot;%d&quot;, res);
    }
    return 0;
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;第三题&lt;/h1&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;给你一个长度为 $n$ 的时间区间 $[left_i, right_i]$，表示会议从 $left_i$ 开始，$right_i$ 天结束。如果小明在其中一天参加会议，那么就算小明参加了这场会议，但是小明每天最多只能参加 $k$ 场会议。问：小明最多能参加多少场会议？&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1\le n \le 10^4$&lt;/li&gt;
&lt;li&gt;$1 \le k \le 10$&lt;/li&gt;
&lt;li&gt;$0 \le left_i \le right_i \le 10^8$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;贪心&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n^2logn)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;贪心策略：按照 $left_i$ 从小达到，若 $left_i$ 则按照区间长度从小到大选择。如果发现 $left_i$ 这天已经被占满了，则更新区间为 $[left_i+1,right_i]$。&lt;/p&gt;
&lt;p&gt;:::note
证明：假设贪心解为集合 $S_1$，最优解为 $S_2$&lt;/p&gt;
&lt;p&gt;则有 $|S_1| \le |S_2|$&lt;/p&gt;
&lt;p&gt;另外，对于集合 $S_1$ 和 $S_2$ 中的每个区间，都有一个生效的天数，因此，分别对 $S_1$ 和 $S_2$ 按照生效天数排序，如果生效天数相等，则按照右端点从小到大排序。&lt;/p&gt;
&lt;p&gt;经过排序后，贪心解存在有第一个和最优解不同的区间，如果 $vaild_i \lt vaild_j$ ，（其中 $valid_i,valid_j$ 分别为贪心解和最优解的有效值，也即是区间中第几天去参加的会议。）那么现在可以将当前区间加入到 $S_2$ 中。&lt;/p&gt;
&lt;p&gt;如果 $valid_i = valid_j$，说明可以将贪心解的该区间把最优解的该区间给替换掉。&lt;/p&gt;
&lt;p&gt;如果 $valid_i \gt valid_j$，这种情况与贪心策略相矛盾&lt;/p&gt;
&lt;p&gt;综上，最优解能通过添加区间或者替换区间的方式变成最优解，说明 $|S_2| \le |S_1|$&lt;/p&gt;
&lt;p&gt;因此，$|S_1|=|S_2|$
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;贪心证明如若有误，欢迎批评指正&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个贪心策略用优先队列维护即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;unordered_map&amp;gt;

using namespace std;

const int N = 10010;


struct Node {
    int l, r, len;
    Node() {};
    Node(int a, int b, int c) {
        l = a, r = b, len = c;
    }
    bool operator &amp;lt; (const Node&amp;amp; a) const {
        if (a.l == l) return len &amp;gt; a.len;
        return l &amp;gt; a.l;
    }
};

priority_queue&amp;lt;Node&amp;gt; Q;
unordered_map&amp;lt;int, int&amp;gt; cnt;

int main() {

    // freopen(&quot;in.txt&quot;, &quot;r&quot;, stdin);
    // freopen(&quot;out.txt&quot;, &quot;w&quot;, stdout);
    int n, k; scanf(&quot;%d%d&quot;, &amp;amp;n, &amp;amp;k);
    for (int i = 0; i &amp;lt; n; i ++ ) {
        int l, r; scanf(&quot;%d%d&quot;, &amp;amp;l, &amp;amp;r);
        Q.push({l, r, r - l + 1});
    }

    int res = 0;
    while (Q.size()) {
        auto q = Q.top(); Q.pop();
     //   cout &amp;lt;&amp;lt; q.l &amp;lt;&amp;lt; &apos; &apos; &amp;lt;&amp;lt; q.r &amp;lt;&amp;lt; &apos; &apos; &amp;lt;&amp;lt; q.len &amp;lt;&amp;lt; endl;
        if (cnt[q.l] &amp;gt;= k) {
            if (q.l &amp;lt; q.r) Q.push({q.l + 1, q.r, q.len - 1});
            continue;
        }
        cnt[q.l] ++ ;
        res ++ ;
    }
    printf(&quot;%d&quot;, res);
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::TIP
如果 $n=10000$,$k=1$,区间都是$[1,1000000000]$
那么，很明显这个时间复杂度就是 $O(n^2logn)$
:::&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;写在最后，第三题有$O(n^2)$ 或者以下做法欢迎讨论&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>深信服笔试</title><link>https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E6%B7%B1%E4%BF%A1%E6%9C%8D%E7%AC%94%E8%AF%95/</link><guid isPermaLink="true">https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E6%B7%B1%E4%BF%A1%E6%9C%8D%E7%AC%94%E8%AF%95/</guid><description>深信服笔试的编程题</description><pubDate>Sat, 24 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::important
第二题难度加倍，操作数有 6 种，黑科技过了。。第三题编辑距离，真离谱，都怀疑数据有问题了，我考完去力扣过了，但是笔试硬是没过！！！🤬&lt;/p&gt;
&lt;p&gt;由于笔试不支持在 IDE 上编辑，因此暂不给出代码
:::&lt;/p&gt;
&lt;h1&gt;第一题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;给你一个字符串，如果有连续的超过三个重复的字符，删掉至小于 3 个，且每次至少删除 3 个重复字符，如果删完之后依然有连续的超过三个重复的字符，则继续利用删除规则删掉至小于 3 个&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;无&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;h3&gt;栈&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设置两个栈，一个是字符栈，一个是对应字符数量栈&lt;/p&gt;
&lt;p&gt;一次遍历字符串，如果 $s[i] != s[i - 1]$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 $3$ 整除栈顶字符数量，那么此时取出栈顶元素即可
&lt;ul&gt;
&lt;li&gt;如果此时的栈顶字符等于 $s[i]$，就将栈顶元素的数量加一即可&lt;/li&gt;
&lt;li&gt;否则将 $s[i]$ 加入栈中即可&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果 3 不整数栈顶字符数量，直接将 $s[i]$ 加入栈中即可&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后注意判断栈顶字符数量是否大于等于 3 即可&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂无&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::warning
下题暂未给出思路以及代码，等复习一手正则表达式
:::&lt;/p&gt;
&lt;h1&gt;第二题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;正则表达式字符串匹配。给你一个字符串 $s$ 和一个字符匹配串 $p$，问 $p$ 是否匹配 $s$ ？&lt;/p&gt;
&lt;p&gt;其中 $p$ 可能出现的字符为: ^,?,+,*, . ,$&lt;/p&gt;
&lt;p&gt;:::note
本场最难的一题
:::&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;字符串长度小于等于 $100$&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;h3&gt;动态规划&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(nm)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在看了相关语法后，在此题中，字符 ^ 只能出现在第一个位置，且 ^ 的后面不能是 +, * , ?。那么剩下的还有 ^, ., $ 以及字母，无论 ^ 后面是什么，都以其后面的为主，因此最开头出现 ^ 直接删掉即可，如果中间或者末尾出现 ^ ，直接返回 false 即可。&lt;/p&gt;
&lt;p&gt;同理，$ 也是一样，其只能出现在末尾位置，如果 $ 出现在中间位置，则直接返回 false；$ 前面同样也不能是 +, *, ?，剩下的^, ., $ 以及字母，都是以前一个字符为主，因此同样可以删掉最后一个 $ 即可&lt;/p&gt;
&lt;p&gt;综上，经过处理后，字符匹配串里面只会有 ? + * . ，这其实就跟力扣的正则表达式差不多了，就是需要多判断两个字符和多一点转移。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂无&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;第三题&lt;/h1&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;有 $T$ 组数组，每个数据给你两个字符串 $s, p$&lt;/p&gt;
&lt;p&gt;这两个字符串都是 DNA 链，也就是说只有 AGCT 四种字符。&lt;/p&gt;
&lt;p&gt;$DNA$ 链有可能发生 替换、删除和增加&lt;/p&gt;
&lt;p&gt;换句话说，这题就是求编辑距离&lt;/p&gt;
&lt;p&gt;:::note
但是就是不对，我都怀疑他的三种操作根本不是编辑距离的三种操作了，或者题目数据有问题
:::&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;字符串长度 $\le 100$&lt;/p&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;动态规则&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(nm)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;设 $f(i,j)$ 为 $s[1-i]$ 和 $p[1-j]$ 的编辑距离，因此 $f(i,j)$ 的计算公式为&lt;/p&gt;
&lt;p&gt;$$
f(i,j)=
\begin{cases}
min{f(i,j-1)+1, f(i-1,j)+1, f(i-1,j-1)+1},  &amp;amp; \text{if } s[i]\neq p[j]  \
min{f(i,j-1)+1, f(i-1,j)+1, f(i-1,j-1)}, &amp;amp; \text{if } s[i]=p[j]&lt;br /&gt;
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂无&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;第四题&lt;/h1&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;有 $T$ 个样例，对于每个样例：&lt;/p&gt;
&lt;p&gt;有一个长度为 $n$ 的序列,接下来给你 $n$ 行数据，让你求这个序列&lt;/p&gt;
&lt;p&gt;每行有两个整数，第一个整数为 $a$，$a$ 是反转序列中的一个数；第二个整数为 $b$，表示在反转序列中 $a$ 的下一个数在第 $b$ 行。如果在反转序列中，$a$ 是最后一个数，则 $b=0$&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$T \le 10$&lt;/li&gt;
&lt;li&gt;$n \le 10^6$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;单链表&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;$O(nT)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于在反转序列中，出了第一个数外，其他数都是上一个数指向其位置。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$n = 3$&lt;/li&gt;
&lt;li&gt;序列为 $[2,4,1] $&lt;/li&gt;
&lt;li&gt;反转序列为 $[1,4,2]$&lt;/li&gt;
&lt;li&gt;$n$行输入数据为:&lt;/li&gt;
&lt;li&gt;$4 \ \  3$&lt;/li&gt;
&lt;li&gt;$1 \ \  1$&lt;/li&gt;
&lt;li&gt;$2 \ \ 0$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以发现除了第 $2$ 个数据，其余行的数都有指向的，因此，第 $2$ 行数据就是单链表的头结点，$a_i$ 表示数据域，$b_i$ 表示指针域，$b=0$，表示 &lt;strong&gt;NULL&lt;/strong&gt; 值。&lt;/p&gt;
&lt;p&gt;因此，只需要找到头结点，然后单链表遍历即可，最后输出反转后的链表。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;暂无&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;吐槽&lt;/h1&gt;
&lt;p&gt;题目的确有的不是很规范。&lt;/p&gt;
</content:encoded></item><item><title>Atcoder ABC367 F 学到新东西啦 ！！</title><link>https://lzleureka.cn/posts/eureka/judge_set_and_multiset_equal/</link><guid isPermaLink="true">https://lzleureka.cn/posts/eureka/judge_set_and_multiset_equal/</guid><description>判断两区间集合或者多重集的方法以及区间集合的大小</description><pubDate>Thu, 22 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Rearrange Query&lt;/h1&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;给你两个长度为 $N$ 的数组，分别为 $A$ 和 $B$，有 $Q$ 个询问，每个询问有四个值，分别为 $l, r, L, R$，问 $A[l-r],B[L-R]$ 的多重集是否相等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;题目链接：https://atcoder.jp/contests/abc367/tasks/abc367_f&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1 \le N,Q \le 2\times10^5$&lt;/li&gt;
&lt;li&gt;$1 \le A_i, B_i \le N$&lt;/li&gt;
&lt;li&gt;$1 \le l \le r \le N$&lt;/li&gt;
&lt;li&gt;$1 \le L \le R \le N$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;p&gt;使用 &lt;strong&gt;Zobrist哈希&lt;/strong&gt; 判断，具体操作如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;取两个较大值 $L,MOD$&lt;/li&gt;
&lt;li&gt;给 $1$ 到 $N$ 的每个值都映射到一个 $[0, L - 1]$ 里面的数，比如 $$&lt;/li&gt;
&lt;li&gt;根据随机好的值求 $A$ 和 $B$ 的前缀值，比如 $S_A(r) = (\sum_{i=1}^r random[A[i]]) % MOD$&lt;/li&gt;
&lt;li&gt;根据前缀和判断两多重集是否相等，如果值不相等为 &lt;em&gt;no-instance&lt;/em&gt;，如果相等，则至少有 $1-\frac{1}{L}$的概率认为其为&lt;em&gt;yes-instance&lt;/em&gt;，因此对于一个具有 $Q$ 次询问的题，通过测试的概率为有 $(1-\frac{1}{L})^Q$（可能有误）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，建议 $L$ 取 $2^{45}$ 或者 $10^{13}$ ，$MOD$ 取 $2^{61}-1$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拓展&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果不是求多重集合，而是求集合呢？？
更简单了，那么就不是求前缀和，&lt;strong&gt;而是求前缀异或&lt;/strong&gt;，同时也不需要 $MOD$ 了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;random&amp;gt;

using namespace std;

random_device rd;
mt19937_64 gen(rd());

using LL = long long;


const int N = 200010;
const LL L = 1e13, MOD = (1ll &amp;lt;&amp;lt; 61) - 1;

LL h[N];
LL A[N], B[N];


LL g(LL x) {
    return (x % MOD + MOD) % MOD;
}

int main() {

    // freopen(&quot;../in.txt&quot;, &quot;r&quot;, stdin);
    // freopen(&quot;../out.txt&quot;, &quot;w&quot;, stdout);
    int n, q; scanf(&quot;%d%d&quot;, &amp;amp;n, &amp;amp;q);
    for (int i = 1; i &amp;lt;= n; i ++ ) h[i] = gen() % L;

    for (int i = 1; i &amp;lt;= n; i ++ ) {
        int x; scanf(&quot;%d&quot;, &amp;amp;x);
        A[i] = (A[i - 1] + h[x]) % MOD;
    }
    for (int i = 1; i &amp;lt;= n; i ++ ) {
        int x; scanf(&quot;%d&quot;, &amp;amp;x);
        B[i] = (B[i - 1] + h[x]) % MOD;
    }

    while (q -- ) {
        int l, r, L, R; scanf(&quot;%d%d%d%d&quot;, &amp;amp;l, &amp;amp;r, &amp;amp;L, &amp;amp;R);
        if (g(A[r] - A[l - 1]) == g(B[R] - B[L - 1])) puts(&quot;Yes&quot;);
        else puts(&quot;No&quot;);
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
证明自行查阅资料
:::&lt;/p&gt;
&lt;h1&gt;[SDOI2009] HH的项链&lt;/h1&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;给你一个长度为 $n$ 的数组 $A$，给你 $q$ 个询问。每个询问有两个整数 $l, r$，问 $A$ 数组中区间 $[l,r]$ 的集合个数&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;题目链接：https://www.luogu.org/problemnew/show/P1972&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1\le n, q \le 10^6$&lt;/li&gt;
&lt;li&gt;$1 \le A_i \le 10^9$&lt;/li&gt;
&lt;li&gt;$1 \le l,r \le n$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;题解&lt;/h2&gt;
&lt;h3&gt;离线+树状数组&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(nlogn)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第一步，首先把所有的询问存储下来，然后根据右端点排序&lt;/p&gt;
&lt;p&gt;第二步，记录项链中每个贝壳其左边第一个相同种类的下标&lt;/p&gt;
&lt;p&gt;第三步，把所有贝壳的种类的贡献都放在最右边的贝壳中&lt;/p&gt;
&lt;p&gt;第四步，处理询问，按照右端点从大到小来处理，如果此时的右端点是大于上一轮右端点，也就意味着当前右端点的后面部分已经没用了，所有需要更新。更新的原则是：1. 转移贡献，根据第二步计算的当前贝壳其左边第一个相同种类的下标贡献+1，当前贝壳贡献减一&lt;/p&gt;
&lt;p&gt;第五步，当前询问的答案是 $query(r) - query(l - 1)$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;unordered_map&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;map&amp;gt;

#define lowbit(x) ((x) &amp;amp; (-x))

using namespace std;

typedef tuple&amp;lt;int, int, int&amp;gt; TIII;

const int N = 1000010;

int tr[N];
int n, q; 
TIII arr[N];
int color[N];
int res[N];
int pre[N];
map&amp;lt;int, int&amp;gt; um;

void add(int x, int val) {
    for (int i = x; i &amp;lt;= n; i += lowbit(i)) tr[i] += val;
}

int query(int r) {

    int res = 0;
    for (int i = r; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main() {
    
    scanf(&quot;%d&quot;, &amp;amp;n);
    for (int i = 1; i &amp;lt;= n; i ++ ) scanf(&quot;%d&quot;, &amp;amp;color[i]);
    
    for (int i = 1; i &amp;lt;= n; i ++ ) {
        if (um.count(color[i])) {
            add(um[color[i]], -1);
            pre[i] = um[color[i]];
        }
        add(i, 1);
        um[color[i]] = i;
    }
    
    scanf(&quot;%d&quot;, &amp;amp;q);
    for (int i = 1; i &amp;lt;= q; i ++ ) {
        int x, y; scanf(&quot;%d%d&quot;, &amp;amp;x, &amp;amp;y);
        arr[i] = {y, x, i};
    }

    sort(arr + 1, arr + 1 + q);
    reverse(arr + 1, arr + 1 + q);

    int last = n;
    for (int i = 1; i &amp;lt;= q; i ++ ) {
        auto [r, l, id] = arr[i];
        while (last &amp;gt; r) {
            if (pre[last]) {
                add(pre[last], 1);
            }
            add(last, -1);
            last -- ;
        }
        res[id] = query(r) - query(l - 1);
    }

    for (int i = 1; i &amp;lt;= q; i ++ ) printf(&quot;%d\n&quot;, res[i]);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
hh，其实可以算左端点的贡献，按照左端点排序，应该更简洁
:::&lt;/p&gt;
</content:encoded></item><item><title>美团技术笔试第二场</title><link>https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E7%BE%8E%E5%9B%A2%E6%8A%80%E6%9C%AF%E7%AC%AC%E4%BA%8C%E5%9C%BA/</link><guid isPermaLink="true">https://lzleureka.cn/posts/%E9%9D%A2%E8%AF%95/%E7%BE%8E%E5%9B%A2%E6%8A%80%E6%9C%AF%E7%AC%AC%E4%BA%8C%E5%9C%BA/</guid><description>美团技术笔试第二场的编程题</description><pubDate>Sun, 18 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;好久没写题了，就 A 了两题 。。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;第一题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;有 $T$ 组测试用例，每次测试用例给你一个 $n$，让你求一个数 $m$，使得最大公因数 $gcd(n,m)$ 为素数&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;p&gt;大概是
$T \le 100$ ， $2 \le n \le 100000$&lt;/p&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;h3&gt;线性筛 + 枚举&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n + T\frac{n}{ln n})$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先用线性筛求出所有的素数，然后枚举所有素数，看是否是 $n$ 的约数&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;

using namespace std;

const int N = 100010;

int p[N], cnt = 0;
bool st[N];

void init(int n) {

    for (int i = 2; i &amp;lt;= n; i ++ ) {
        if (!st[i]) p[cnt ++ ] = i;
        for (int j = 0; i &amp;lt;= n / p[j]; j ++ ) {
            st[i * p[j]] = true;
            if (i % p[j] == 0) break;
        }
    }
}

void solve() {
    int n; scanf(&quot;%d&quot;, &amp;amp;n);
  //  cout &amp;lt;&amp;lt; cnt &amp;lt;&amp;lt; endl;
    for (int i = 0; p[i] &amp;lt;= n; i ++ ) 
        if (n % p[i] == 0) {
            printf(&quot;%d\n&quot;, p[i]);
            break;
        }
}

int main() {

    // freopen(&quot;in.txt&quot;, &quot;r&quot;, stdin);
    // freopen(&quot;out.txt&quot;, &quot;w&quot;, stdout);

    init(N - 1);
    int T; scanf(&quot;%d&quot;, &amp;amp;T);
    while (T -- ) {
        solve();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
hh，事实上线性筛本质上就是用一个合数的最小素数因数将该合数筛掉，因此只需要保存每个数的最小素数因子即可。所以最后的枚举多此一举，时间复杂度应该为 $O(n+T)$
:::&lt;/p&gt;
&lt;h1&gt;第二题&lt;/h1&gt;
&lt;h2&gt;题意描述&lt;/h2&gt;
&lt;p&gt;给你一个长度为 $n$ 的数组，然后你可以执行无限次以下操作&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;选择数组下标不同的两个数 $a_i,a_j$，使得数组中 $a_i \rightarrow a_i - 1$，$a_j \rightarrow a_j+1$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;求使得使得这个数组极差最小的&lt;strong&gt;最少&lt;/strong&gt;操作数&lt;/p&gt;
&lt;p&gt;:::note
极差：数组中最大值和最小值之差
:::&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$n \le 100000$&lt;/li&gt;
&lt;li&gt;$1 \le a_i \le 10^9$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;h3&gt;思维&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(nlogn)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;可以想象这个数组极差最小是多少？一定是 $0$ 或者 $1$&lt;/p&gt;
&lt;p&gt;假设此时极差不为 $0$ 或者 $1$，也就是最大值 $a_{max}$ 和最小值 $a_{min}$ 之差 $\ge 2$ ，那么此时可以通过一次操作将这两个数的差降到 $0$ 或者 $1$。如果进行了此次调整，极差仍然不为 $0$ 或者 $1$，继续执行改步骤即可&lt;/p&gt;
&lt;p&gt;另外，假设 $S = \sum a_i$ ，由于每次操作都有一个数减一，一个数加一，因此每次操作 $S$ 不变。&lt;/p&gt;
&lt;p&gt;因此，如果 $S % n = 0$，说明最后极差一定为 $0$，且每个数都为 $S / n$；如果 $S % n \neq 0$，说明最后极差一定为 $1$，且有 $S % n$ 个数为 $\frac{S}{n} + 1$ ，$n - S % n$ 个数为 $S / n$&lt;/p&gt;
&lt;p&gt;根据贪心性质，将数组从大到小排序，前面 $S % n$ 个数变成 $\frac{S}{n} + 1$ 和后面的$n - S % n$ 个数变成 $S / n$ 需要操作数为 $res$&lt;/p&gt;
&lt;p&gt;最终答案就是 $res / 2$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;
#include &amp;lt;algorithm&amp;gt;

using namespace std;

typedef long long LL;

const int N = 100010;

int q[N];
int n; 

int main() {

    // freopen(&quot;in.txt&quot;, &quot;r&quot;, stdin);
    // freopen(&quot;out.txt&quot;, &quot;w&quot;, stdout);


    scanf(&quot;%d&quot;, &amp;amp;n);
    LL sum = 0;
    for (int i = 0; i &amp;lt; n; i ++ ) scanf(&quot;%d&quot;, &amp;amp;q[i]), sum += q[i];
    sort(q, q + n);
    reverse(q, q + n);
    LL t1 = sum / n, t2 = sum % n;

    LL res = 0;
    for (int i = 0; i &amp;lt; n; i ++ )
        if (i &amp;lt; t2) res += abs(q[i] - t1 - 1);
        else res += abs(q[i] - t1);
    printf(&quot;%lld\n&quot;, res / 2);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
注意开 long long
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;:::warning
下题暂未给出代码，仅给出目前思路，等 A 了之后再上传
:::&lt;/p&gt;
&lt;h1&gt;第三题&lt;/h1&gt;
&lt;h2&gt;题目大意&lt;/h2&gt;
&lt;p&gt;给你一个长度为 $n$ 的数组，其数组之和为$\sum a_i$，然后接下来有两个人顺序执行两个操作&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;小红先选择一段区间 $[l,r]$，对该区间的每个数都乘 $k$，小红希望选择的区间能够让最后的 $sum$ 尽可能大&lt;/li&gt;
&lt;li&gt;小明再选择一段区间 $[l,r]$，对该区间的每个数都乘 $k$，小明希望选择的区间能够让最后的 $sum$ 尽可能小&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;问：在经过这两个操作之后，$sum$ 的值为多少？&lt;/p&gt;
&lt;h2&gt;数据范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$1\le n \le 1000$&lt;/li&gt;
&lt;li&gt;$-10^5 \le k \le 10^5$&lt;/li&gt;
&lt;li&gt;$-10^5 \le a_i \le 10^5$&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;问题一&lt;/strong&gt;   假设最开始的数组累和为 $sum$，修改的区间是 $[l,r]$，将其每个值都乘 $k$，最后的 $sum_c$ 是多少？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;：$sum_c = sum + (k - 1) * \sum_{i=l}^r a_i$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;问题二&lt;/strong&gt;   $sum$ 值难道不是取最大值或者最小值吗？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;答&lt;/strong&gt;：这里显然不是，$sum$ 是在题目限定下一个确切的值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设小红已经做出选择，根据问题一，小明只需要找一个最大或者最小的连续字段和便可。因此，小红要想最后的 $sum$ 取最大值，是所有区间里面，最后的 $sum$ 最大的区间&lt;/p&gt;
&lt;h3&gt;暴力&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n^3)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据上述分析，有一个很暴力的解法就是枚举小红选择的区间，然后小明对每个修改后的数组求一遍最大或者最小子段和，然后所有区间能求得的最大值就是 $sum$&lt;/p&gt;
&lt;h3&gt;线段树优化&lt;/h3&gt;
&lt;p&gt;---Update: 2024-08-28
:::important
hh，这题事实上线段树就解决了，下面的分析完全不需要。
暴力枚举小红的区间，然后每个区间乘 $k$，线段树可以 $logn$ 更新；然后求最大/小连续子区间也可以线段树求。&lt;/p&gt;
&lt;p&gt;数据结构太弱了 ~
:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间复杂度 $O(n^2 logn)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先先把特殊情况去掉，如果 $k = 1$ ，则无论怎么样，$sum$ 的值都不会变，因此情况分为 $k &amp;gt; 1$ 和 $k &amp;lt; 1$；以下分析只考虑 $k &amp;gt; 1$ 的情况， $k &amp;lt; 1$ 的分析类似。&lt;/p&gt;
&lt;p&gt;上面这个暴力的时间复杂度问题在于枚举了小红选择的所有区间，并且小红和小明最后所选的区间也没有任何联系，那么能否优化这部分？&lt;/p&gt;
&lt;p&gt;答案是肯定的。我们仔细想一想，小红和小明选择的区间是否存在交叉的情况&lt;/p&gt;
&lt;p&gt;:::tip
区间A: [1,5]，区间B:[2,6]。区间 A B 表示交叉，如果 A 完全包含 B 或者 B 完全包含 A，不表示交叉；同时，A 和 B 没有交集也表示不交叉。
:::&lt;/p&gt;
&lt;p&gt;假设最后选择中，小红和小明选择的区间是由交叉部分的，如果交叉部分的和的值是等于 0 的，无论是小红还是小明，加上这个区间都没有意义，相当于两人的选择是没有交集的；如果交叉部分的和的值是大于 0 的，那么对于小明来说，不选择这个区间比选择这个区间更优，因此可以去掉这个区间；如果交叉部分的和的值是小于 0 的，那么对于小红来说，这部分区间反而给小明利用了，因此不选择这部分区间比选择这部分区间更优&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;综上，小红小明最终选择的两区间，一定是不交叉的。（注意可以是包含关系）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此这里分为三种情况考虑：&lt;/p&gt;
&lt;p&gt;假设同样枚举小红区间为 $[l,r]$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;小红与小明区间没有交集&lt;/p&gt;
&lt;p&gt;小明则无非是选择 $[1,l-1]$ 或者 $[r + 1, n]$ 的最小子段和&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;小明区间完全包含小红区间&lt;/p&gt;
&lt;p&gt;小明无非就是选择以 $[1,l-1]$ 中以 $l-1$ 为终点的最小子段和的区间以及 $[r + 1, n]$ 中以 r + 1 为起点的最小子段和的区间以及 $[l,r]$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;小红区间完全包含小明区间&lt;/p&gt;
&lt;p&gt;$[l,r]$ 区间的最小字段和&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到上述 1、2 好求，但是 3 目前只能想到用线段树来维护，因此每次求这个区间最小字段和的时间复杂度为 $O(logn)$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;写在最后，第三题有更好的解法欢迎讨论&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;吐槽&lt;/h1&gt;
&lt;p&gt;我太菜了，果然只有对算法融会贯通才能写出第三题嘛 ┭┮﹏┭┮&lt;/p&gt;
</content:encoded></item><item><title>与树相关</title><link>https://lzleureka.cn/posts/graphtheory/%E4%B8%8E%E6%A0%91%E7%9B%B8%E5%85%B3/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/%E4%B8%8E%E6%A0%91%E7%9B%B8%E5%85%B3/</guid><description>关于树的一些内容</description><pubDate>Sun, 30 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;树的直径&lt;/h1&gt;
&lt;h2&gt;树的直径必须边&lt;/h2&gt;
&lt;p&gt;对于任意一条直径 $a_0,a_1,\cdots,a_k$ ，假设找到最大的 $left$ $a_{left}$ 可以替换 $a_0,a_1,\cdots,a_{left}$ 的部分，最小的 $right$ $a_{right}$ 可以替换 $a_{right},\cdots,a_k$ 部分，那么 $right - left$ 就是必须边&lt;/p&gt;
</content:encoded></item><item><title>网络流</title><link>https://lzleureka.cn/posts/graphtheory/%E7%BD%91%E7%BB%9C%E6%B5%81/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/%E7%BD%91%E7%BB%9C%E6%B5%81/</guid><description>网络流的各种内容 ~ </description><pubDate>Thu, 25 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;概念&lt;/h1&gt;
&lt;p&gt;流网络$G=(V,E)$：一个有向图，有源点和汇点，每条边上的权值为流量，表示这条边每次能通过的最大流量。就好比有水从源点源源不断地流向汇点，在这个过程中，水会流向多个管道，但是每条管道都会有流量限制。&lt;strong&gt;不考虑反向边&lt;/strong&gt;。如果有反向边，也可以通过加点的方式变成没有反向边。&lt;/p&gt;
&lt;p&gt;可行流 $f$ ：满足 1. 容量限制 $0\le f(u,v) \le c(u,v)$，2. 流量守恒（除了源点和汇点之外，其余所有点都不存储流）$v,x \in {V / {s,t} } \ \ \sum_{(v,x)\in E} f(v,x) = \sum_{(x,v)\in E} f(x,v)$  。这里&lt;strong&gt;不考虑反向边&lt;/strong&gt;。$|f|$ 表示从源点到汇点的流量值是多少，或者说速率。$|f|=\sum_{(s,v)\in E}f(s,v) - \sum_{(v,s) \in E}f(v,s)$ ，简单地说就是源点往外流的流量减去往源点流的流量。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/%E6%9C%80%E5%A4%A7%E6%B5%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最大流&lt;/strong&gt; ：所有可行流中流量值最大的可行流，也就是最大可行流&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;残留网络&lt;/strong&gt;$G_f$：残留网络是针对流网络的某一条可行流来说的，残留网络里面的点是原网络的点集，残留网络的边集是原来网络里面的边集和反向边集，权值的定义为：
$$
c^\prime(u,v)
\left{
\begin{aligned}
&amp;amp;c(u,v)-f(u,v) &amp;amp; (u,v)\in E \
&amp;amp;f(u,v) &amp;amp; (v,u) \in E \
\end{aligned}
\right.
$$
残留网络可能也有可行流 $f^\prime$ ，&lt;strong&gt;原网络的可行流+残留网络的可行流也是原图的可行流&lt;/strong&gt;，因此 $|f+f^\prime|=|f|+|f^\prime|$
可行流相加等价于边相同方向则权值累加，边相反则原图边减去残留网络边的权值&lt;/p&gt;
&lt;p&gt;$f+f^\prime$ 是否是原图可行流？判断是否满足容量限制和流量守恒即可&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;残留网络里面没有可行流，原网络可行流一定是最大流&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;增广路径&lt;/strong&gt;： 在残留网络里面，沿着容量大于 $0$ 的边走，如果能走到终点，则这条路径被称为增广路径&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;割&lt;/strong&gt;：将点集 $V$ 分成 $S$ 和 $T$ 两个子集，使得 $S\cup T = V$ 并且 $S \cap T = \emptyset$ ，$s \in S$，$t \in T$ 。换句话说，把点集分成不重的两个部分，源点在 $S$ 集中，汇点在 $T$ 集中，划分的这个结果称为图中的一个割。点集不一定连通。
任意图一共有 $2^{n-2}$ 个割&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/%E5%89%B2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;割的容量$c(S,T)$：指的是划分后的 $S$ 集中点到 $T$ 集中的边的权值之和&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最小割&lt;/strong&gt;：所有割中容量最小的割&lt;/p&gt;
&lt;p&gt;割的流量$f(S,T)$：割的流量是对一个可行流来说的，就是 $S$ 到 $T$ 的边的所有可行流值减去 $T$ 到 $S$ 的边的所有可行流值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;割的性质&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任意一个割$[S,T]$和任意一个流$f$，一定有 $f(S,T) \le c(S,T)$&lt;/li&gt;
&lt;li&gt;任意一个割$[S,T]$和任意一个流$f$，一定有 $f(S,T) = |f|$
直观地思考一下，$s$ 流到 $t$ 的容量等价于 $S$ 流到 $T$ 的净流量&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;稍微证明：设 $f(X,Y) = \sum_{u\in X}\sum_{v \in Y} f(u,v) - \sum_{u\in X}\sum_{v \in Y} f(v,u)$
那么，有$f(X,Y) = -f(Y,X)$，$f(X,X)=0$，
如果，$X\cap Y = \emptyset$，$f(Z,X\cup Y)=f(Z,X)+f(Z,Y)$，同样地 $f(X\cup Y,Z)=f(X,Z)+f(Y,Z)$
因此，$f(S,V)=f(S,S)+f(S,T)$，也就是 $f(S,T)=f(S,V)$
从而有
$$
f(S,T)=f({s},V)+f(S-{s}, V)
$$
由于 $S - {s}$ 集合不包含源点 $s$ 和汇点 $t$，根据流量守恒，$f(S-{s},V)=0$
因此 $f(S,T)=f({s},V)=|f|$&lt;/p&gt;
&lt;p&gt;推论3. 任意一个割$(S,T)$和任意一个流$f$，有 $|f| \le c(S,T)$&lt;/p&gt;
&lt;p&gt;从而得知：最大流 $\le$ 最小割&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最大流最小割定理：&lt;/strong&gt;
（1）可行流 $f$ 是最大流
（2）可行流 $f$ 的残留网络中不存在增广路
（3）存在某个割 $[S,T]$ ，$|f|=c(S,T)$
三个条件两两等价&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;证明：
（1）$\Longrightarrow$ （2）。可以用反证法证明，如果存在增广路，那么这个残留网络的可行流 $|f^\prime| \gt 0$ ，因此 $f+f^\prime$ 是一个更大的可行流，这与 $f$ 是最大流矛盾&lt;/p&gt;
&lt;p&gt;（3）$\Longrightarrow$ （1）。根据上面的推论3，最大流 $\le c(S,T)$ ，而又因为有 $|f| = c(S,T)$ ，因此最大流 $\le |f|$ ，又根据定义 $|f| \le$ 最大流。从而，存在有某个割 $[S,T]$，$|f|=c(S,T)$ ，那么 $f$ 是最大流。从这里还能发现一个隐含的等于，由于最大流 $\le$ 最小割，又有 最小割  $\le c(S,T) = |f| \le$ 最大流。因此，&lt;strong&gt;最大流=最小割&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;（2）$\Longrightarrow$（3）。用构造来证明，就是在残留网络中不存在增广路能否构造出一个割，使得 $|f|=c(S,T)$。我们构造 $S$ 为在 $G_f$ 中沿着容量大于 0 的边所有能走到的点，$T = V - S$，这样子 $s\in S$，$t\in T$ 是一个合法的割。注意：割虽然是根据残留网络构造，但是划分里面的边还是原网络里面的边。&lt;/p&gt;
&lt;p&gt;对于 $x \in S, y \in T$，如果 $(x,y) \in E$ ，那么 $f(x,y)=c(x,y)$，因为从构造角度来看，在残留网络中 $(x,y)$ 的权值为 $c(x,y)-f(x,y)$，如果这个值是大于0，则 $y \notin T$ ，这与划分矛盾。
同样地，对于 $a\in T, b \in S$，如果 $(a,b) \in E$，那么 $f(a,b)=0$，因为从构造的角度来看，在残留网络中，如果 $f(a,b) \gt 0$  ，那么其建立的反向边 $f(b,a)=f(a,b) &amp;gt; 0$ ，则 $a \notin T$ ，这与划分矛盾。&lt;/p&gt;
&lt;p&gt;从而 $|f|=f(S,T)=\sum_{u\in S}\sum_{v \in T} f(u,v) - \sum_{u\in S}\sum_{v \in T} f(v,u) =  \sum_{u\in S}\sum_{v \in T} f(u,v) = \sum_{u\in S}\sum_{v \in T} c(u,v)$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/%E6%9C%80%E5%A4%A7%E6%B5%81%E6%9C%80%E5%B0%8F%E5%89%B2%E8%AF%81%E6%98%8E.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;算法&lt;/h1&gt;
&lt;p&gt;无论什么算法，都逃不过 FF 方法，也就是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找增广路径&lt;/li&gt;
&lt;li&gt;更新残留网络
似乎有 ISAP 算法可替代，HLPP 算法优化很大，常用背 dinic 算法即可&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;EK 算法&lt;/h2&gt;
&lt;p&gt;时间复杂度 $O(nm^2)$，处理 1000~10000 的网络没有问题
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;

using namespace std;

const int N = 1010, M = 20010, INF = 0x3f3f3f3f;

int n, m, S, T;
int h[N], e[M], ne[M], f[M], idx = 0;
bool st[N];
int d[N], q[N], pre[N];

void add(int a, int b, int c) {
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int bfs() {
    memset(st, 0, sizeof st);
    int hh = 0, tt = 0;
    q[0] = S; st[S] = true; d[S] = INF;
    
    while (hh &amp;lt;= tt) {
        int u = q[hh ++ ];
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (!st[v] &amp;amp;&amp;amp; f[i]) {
                st[v] = true;
                d[v] = min(d[u], f[i]);
                pre[v] = i;
                if (v == T) return true;
                q[++ tt] = v;
            }
        }
    }
    return false;
}

int EK() {
    
    int ans = 0;
    while (bfs()) {
        ans += d[T];
        for (int i = T; i != S; i = e[pre[i] ^ 1])
            f[pre[i]] -= d[T], f[pre[i] ^ 1] += d[T];
    }
    return ans;
}

int main() {
    
    memset(h, -1, sizeof h);
    
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m &amp;gt;&amp;gt; S &amp;gt;&amp;gt; T;
    while (m -- ) {
        int a, b, c; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
        add(a, b, c), add(b, a, 0);
    }
    
    cout &amp;lt;&amp;lt; EK() &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;dinic 算法&lt;/h2&gt;
&lt;p&gt;时间复杂度 $O(n^2m)$ ，处理 10000 ~ 100000 的网络没有问题&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;bfs&lt;/code&gt; 建立分层图&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dfs&lt;/code&gt; 找出所有能增广的路径
代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cstring&amp;gt;

using namespace std;

const int N = 10010, M = 200010, INF = 0x3f3f3f3f;

int n, m, S, T;
int h[N], e[M], ne[M], f[M], idx = 0;
int q[N], d[N], cur[N];

void add(int a, int b, int c) {
    e[idx] = b, f[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

bool bfs() {
    int hh = 0, tt = 0;
    memset(d, 0, sizeof d);
    d[S] = 1, q[0] = S, cur[S] = h[S];
    
    while (hh &amp;lt;= tt) {
        int u = q[hh ++ ];
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (!d[v] &amp;amp;&amp;amp; f[i]) {
                d[v] = d[u] + 1;
                cur[v] = h[v];
                if (v == T) return true;
                q[++ tt] = v;
            }
        }
    }
    return false;
}

int find(int u, int limit) {
    
    if (u == T) return limit;
    
    int flow = 0;
    for (int i = cur[u]; ~i &amp;amp;&amp;amp; flow &amp;lt; limit; i = ne[i]) {
        cur[u] = i;
        int v = e[i];
        if (d[v] == d[u] + 1 &amp;amp;&amp;amp; f[i]) {
            int t = find(v, min(f[i], limit - flow));
            if (!t) d[v] = 0;
            flow += t, f[i] -= t, f[i ^ 1] += t;
        }
    }
    return flow;
}

int dinic() {
    int r = 0, flow;
    while (bfs()) while (flow = find(S, INF)) r += flow;
    
    return r;
}

int main() {
    
    memset(h, -1, sizeof h);
    
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m &amp;gt;&amp;gt; S &amp;gt;&amp;gt; T;
    
    while (m -- ) {
        int a, b, c; cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
        add(a, b, c); add(b, a, 0);
    }
    
    cout &amp;lt;&amp;lt; dinic() &amp;lt;&amp;lt; endl;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;最大流模型&lt;/h1&gt;
&lt;h2&gt;二分图模型&lt;/h2&gt;
&lt;h3&gt;二分图最大匹配&lt;/h3&gt;
&lt;p&gt;二分图的最大匹配可以转化成一个网络流问题，时间复杂度为 $O(m\sqrt n)$&lt;/p&gt;
&lt;p&gt;建图思路为：首先考虑一个源点 $S$ 和汇点 $T$，然后对于 $V_1$ 集中的点，每个点最多只会选择一次进行匹配，因此可以把 $S$ 和 $V_1$ 集中的每个点连接一条容量为 $1$ 的边，表示这条边对应的点要么不选，要么只选一次；然后对于 $V_2$ 集中的点，同样也最多只会选择一次进行匹配，因此可以将每个点向汇点连接一条容量为 $1$ 的边；最后原来 $V_1$ 和 $V_2$ 中的边都设置容量为 $1$ 。最后对这个转化后的图求得的最大流就是二分图最大匹配&lt;/p&gt;
&lt;p&gt;证明：
首先证明二分图的一个匹配可以转化为新图中的一个可行流。显然满足流量守恒和容量限制。
其次证明新图中的一个整数可行流也可以转化成二分图的一个匹配。对于一个可行流，由于容量限制， $V_1$ 的出度一定要么是0，要么是 1，$V_2$ 的入度要么是 0 要么是 1，并且 $V_1$ 出度为 1 的点一定流向 $V_2$ 入度为 1 的点，这些流量为 1 的边就是一个匹配&lt;/p&gt;
&lt;h3&gt;二分图多重匹配&lt;/h3&gt;
&lt;p&gt;二分图的多重匹配指的是每个点可以多次匹配，但是有上限，或者说限制了每个点最多和多少条匹配边相关联&lt;/p&gt;
&lt;p&gt;这个建图思路和上面类似，首先建立源点和汇点，然后源点连接一个点集，容量为点集 $V_1$ 能够匹配上限，点集 $V_2$ 连接汇点，容量为能够匹配上限。中间 $V_1$ 到 $V_2$ 的边按照原图连接，不过&lt;strong&gt;容量为题目所限制的&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;上下界可行流&lt;/h2&gt;
&lt;h3&gt;无源汇&lt;/h3&gt;
&lt;p&gt;无源汇上下界可行流可以做一个转化变成一个正常最大流的一个限制
$(G,f) \Longrightarrow (G^\prime, f^\prime)$&lt;/p&gt;
&lt;p&gt;简单来说，对于上下界可行流中的边可行流限制 $c_L(u,v) \le f(u,v) \le c_U(u,v)$ ，不等式两边减去其下界可得$0 \le f(u,v) \le c_U(u,v) - c_L(u,v)$ ，这样就变成了正常的最大流的容量限制。显现这个是满足容量限制的，但是不满足流量守恒。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/%E4%B8%8A%E4%B8%8B%E7%95%8C%E5%8F%AF%E8%A1%8C%E6%B5%81.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设存在这样的上下界可行流，那么对原网络进行转化后，每条边的流量要减去这条边的容量下界，因此此时不一定满足流量守恒。
单从某个点来说，原网络的流量守恒有 $\sum_{v\in V} C(x,v) = \sum_{v\in V}C(v,x)$ ，但是由于这里的容量变小了，令 $C_I = \sum_{c\in V} C^\prime(v,x),C_O = \sum_{c\in V} C^\prime(x,v)$ ，也就是说
$\sum_{v\in V} C(x,v) - C_O \neq \sum_{v\in V}C(v,x) - C_I$ 时，流量不守恒，我们需要执行操作使其守恒，也就是说，&lt;strong&gt;如果 $C_I \gt C_O$，需要从源点 $S$ 连接一条到点 $x$ 的一条容量为 $C_I-C_O$ 的边，否则从 $x$ 连接到一条到汇点 $T$ 的一条容量为 $C_O - C_I$ 的边，最后判断其最大流在其新建边上是否满流即为存在上下界可行流&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此建图方案是否可行等价于证明原网络的可行流可以变成新网络的最大流，新网络的最大流且满流是否可以还原成原网络的可行流。&lt;/p&gt;
&lt;h3&gt;有源汇&lt;/h3&gt;
&lt;p&gt;有源汇与无源汇的最大的区别在于有源汇中的源点和汇点流量是不守恒的，因此，此处的处理方法为&lt;strong&gt;汇点连接源点一条容量为无穷大的边&lt;/strong&gt;，这样一定是满足流量限制且是无源汇的。&lt;/p&gt;
&lt;p&gt;通过这样的转化后，要想判断是否有源汇上下界可行流，就判断新图是否满流，那么如何求这样的最大可行流呢？&lt;/p&gt;
&lt;p&gt;有一个想当然的思路是：当进行对新图进行一遍dinic 算法之后，然后判断满流是否有可行流。再在这个新图上，把新加的原图中汇点到源点容量为正无穷的边删掉，再做一遍从原图中源点到汇点的dinic算法。如果求最小可行流就是最后做一遍汇点到源点的dinic算法，并减去即可。&lt;/p&gt;
&lt;p&gt;这个方法从策略上是正确的，但是要注意从逻辑上区分，只是我们在这里恰好能做。&lt;/p&gt;
&lt;p&gt;证明：$(G,f)\iff (G^\prime,f^\prime)$&lt;/p&gt;
&lt;p&gt;对于原图任意的可行流，可以变成新图的一个满流。$(G,f_0)\Longrightarrow (G^\prime, f_0^\prime)$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;做题细节&lt;/strong&gt;
注意：有源汇上下界分为最小流和最大流，另外最终的可行流流量可能为负数，假如 $S-&amp;gt;T$ 的流量为 0，$T-&amp;gt;S$ 的流量为 10，$S-&amp;gt;T$ 的最小流就是 $-10$
对于最大流和最小流，都是要建立一条汇点到源点容量为无穷大的边，然后跑完第一次dinic算法之后，判断其是否满流。注意，这个时候如果我们还原成原图的可行流，其中的可行流流量是多少？就是新加的汇点到源点中&lt;strong&gt;残余网路反向边的容量&lt;/strong&gt;，这个时候把这个汇点到源点的边删除。&lt;strong&gt;从原来的源点跑汇点就是最大流，从源点的汇点跑到源点就是最小流&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;多源汇最大流&lt;/h2&gt;
&lt;p&gt;思路和最短路是思路是一样的，建立超级源点和超级汇点，其容量为无穷大即可。&lt;/p&gt;
&lt;h2&gt;关键边&lt;/h2&gt;
&lt;p&gt;最大流的关键边是如果我们把这条边的容量上界提升，那么最大流增加。
求解最大流关键边的步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先应用 dinic 算法求解最大流，那么残余网络正向边的容量为 0 的边可能是关键边&lt;/li&gt;
&lt;li&gt;分别对源点和汇点应用 dfs。对于源点来说，找到这个点只走残余网络正向边容量大于 0 的边能到的点；对于汇点来说，通过反向边遍历，限制是其对应的正向边的容量大于 0，找到所有能遍历到的点&lt;/li&gt;
&lt;li&gt;枚举所有满流的边，如果源点能走到它且汇点也能走到它，那么这就是关键边&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;利用网络流进行判断&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;最小化最大值类型&lt;/li&gt;
&lt;li&gt;判断是否可行&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;拆点技巧&lt;/h2&gt;
&lt;p&gt;如果某些题对点具有限制，那么便可以进行拆点&lt;/p&gt;
&lt;h1&gt;最小割模型&lt;/h1&gt;
&lt;p&gt;割：$[S,T]$
割的流量 ：$f(S,T)$
割的容量：$c(S,T)$&lt;/p&gt;
&lt;h2&gt;最大权闭合图&lt;/h2&gt;
&lt;p&gt;闭合图是对于有向图来说的， 也就是$G(V,E)$ 中的一个点集，这个点集相关的所有边，不能从点集里面指向点集外面，如下图红色圈内所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/%E9%97%AD%E5%90%88%E5%AD%90%E5%9B%BE.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点集加上点集内部的边被称为原图的一个闭合子图，那么最大权闭合子图就是&lt;strong&gt;点的权值之和&lt;/strong&gt;最大的闭合子图&lt;/p&gt;
&lt;p&gt;做法就是转化成最小割模型，首先将其转化成流网络，如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/%E9%97%AD%E5%90%88%E5%AD%90%E5%9B%BE%E4%B9%8B%E6%B5%81%E7%BD%91%E7%BB%9C.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以证明原图的一个闭合子图对应流网络的一个割
对于这个问题本身有个&lt;strong&gt;简单割&lt;/strong&gt;的概念：指的是所有的割边一定是和源点或者汇点相连 ，现在简单割是所有割的子集&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最小割一定是简单割&lt;/strong&gt;。因为最大流最小割定理，最小割一定是有限值，而且中间的边都是无穷，因此最小割一定是简单割&lt;/p&gt;
&lt;p&gt;现需要证明 &lt;strong&gt;闭合子图对应一个简单割&lt;/strong&gt;
假设有闭合子图 $V^\prime$，其对应的割 $[S,T]$，其中 $S = V^\prime +{s}, T = V - S$ ，因为 $V^\prime$ 是所有出边都是在 $V^\prime$ 里面，因此，割边不存在容量为无穷的边，因此这个闭合子图对应一个简单割&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;数量关系&lt;/strong&gt;：$c[S,T]=c[V^\prime,{t}]+c[{s},\overline{V^\prime}]=\sum_{v\in {V^\prime} ^-}{(-w_v)} + \sum_{v\in {\overline{V}^\prime}^+}w_v$
又闭合子图的权值之和为 $W(V^\prime)=\sum_{v\in {V^\prime} ^+}{(w_v)} - \sum_{v\in {V^\prime}^-}(-w_v)$
因此，$c[S,T]+W(V^\prime)=$ 所有的正权值之和
由于等式右边是定值，所有要想 $W(V^\prime)$ 最大，$c[S,T]$ 得最小，等价于求最小割&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案&lt;/strong&gt;
最大闭合子图就是 $S$ 通过残余网络能到的所有点&lt;/p&gt;
&lt;h2&gt;最大密度子图&lt;/h2&gt;
&lt;p&gt;在无向图里面选择一些点和一些边，这个点集都是在边里面的，其密度为 $\frac{|E^\prime|}{|V^\prime|}$ ，求密度最大的子图&lt;/p&gt;
&lt;p&gt;这个一般是 [[最短路模型#0/1 分数规划]] 问题，因此二分一个 $mid$  ，因此如果存在 $\frac{|E^\prime|}{|V^\prime|} &amp;gt; mid$ ，也就是 $mid * |V^\prime|  - |E^\prime| &amp;lt; 0$   因此相当于是求 $mid * |V^\prime| - |E^\prime|$  的最小值，如果其小于 0 ，等价于存在 $\frac{|E^\prime|}{|V^\prime|} &amp;gt; mid$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拓展&lt;/strong&gt; 事实上这个问题可以转化成最大权闭合图，根据&lt;strong&gt;选择的边其相应的点一定要选择&lt;/strong&gt;，可以将边也看作点，不过要求的是 是否存在 $|E^\prime| - mid * |V^\prime|$ 的最大值大于 0，这样才等价于最大权&lt;/p&gt;
&lt;p&gt;有这样一个性质：&lt;strong&gt;如果选择了一些点，其内部边一定全部选择才能使其密度最大&lt;/strong&gt;，因此可以得到其计算公式：
$$
mid * |V^\prime| - |E^\prime| = \sum_{v \in V^\prime} mid - \frac{\sum_{v\in V^\prime}d_v - c[v^\prime,\overline{v}^\prime]}{2}
$$
边的计算方法就是其点的度数之和减去割的容量除2，因为是在无向图中。这样就和割关联起来&lt;/p&gt;
&lt;p&gt;等式变形：
$$
mid * |V^\prime| - |E^\prime| = \frac{1}{2}(\sum_{v\in V^\prime} (2&lt;em&gt;mid - d_v) + c[v^\prime,\overline{v}^\prime])
$$
但是问题在于左边这项怎么处理，其是通过每个点都向汇点连接一条容量是 $2&lt;/em&gt;mid-d_v$ 的边，原图内部边容量为 1，然后源点向每个点连接一条容量为 1 的边。&lt;/p&gt;
&lt;p&gt;但是这里出现了另外一个问题，就是 $2*mid-d_v$ 可能小于0，因此这里有个策略是源点连接所有点的边的容量和所有点连接汇点的边的容量都加上一个偏移量 $U$ ，&lt;strong&gt;但是，注意，不是所有图都能这样子加，这是因为这个问题的特殊性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可以计算，最后 $c[S,T]=U&lt;em&gt;n + 2&lt;/em&gt; mid |V^\prime|-2|E^\prime|$
因此，求出变化后的流网络的最小割之后，其原网络的最小割为 $\frac{c[S,T] - U*n}{2}$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果有边权怎么办？&lt;/strong&gt;
就是每条边有大于 0 的权值，区别在于 $|E^\prime|$ 的计算，其中 $d_v$ 不再是每个点的度，而是与其相关所有边的权值之和&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果同时有点权和边权怎么办&lt;/strong&gt;？
一般来说，如果同时存在会最大化 $\frac{|E^\prime|+|V^\prime|}{|V^\prime|}$ ，等价于最小化
$$
mid*|V^\prime|-|V^\prime|-|E^\prime|=\frac{1}{2}(\sum_{v\in V^\prime} (2&lt;em&gt;mid - 2&lt;/em&gt;p_i- d_v) + c[v^\prime,\overline{v}^\prime])
$$
和上面比较不过是稍微修改了下流网络的权值&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方案&lt;/strong&gt;
最大密度子图的方案为：$S$ 通过残余网络能到的所有点&lt;/p&gt;
&lt;h2&gt;最小权点覆盖集&lt;/h2&gt;
&lt;p&gt;假设所有点权值非负，求总权值之和最小的点覆盖集
这在一般图中是 NP-hard ，但是在二分图中不是&lt;/p&gt;
&lt;p&gt;建图：二分图原图连接的边的容量为无穷大，源点到左部集的边的容量为对应点的权值，右部集到汇点的边的容量为对应点的权值&lt;/p&gt;
&lt;p&gt;可以证明：一个简单割对应一个点覆盖集&lt;/p&gt;
&lt;p&gt;同样：也可以看出一个割的容量对应了一个点覆盖集的权值之和，因此，&lt;strong&gt;求二分图的最小权点覆盖就等价于求新图的最小割&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;求方案：首先根据最大流求出 $S$ 和 $T$ 集合，然后求割边，已经割边是最小割，要么源点是 $S$ ，要么汇点是 $T$ ，因此&lt;/p&gt;
&lt;h2&gt;最大权独立集合&lt;/h2&gt;
&lt;p&gt;注意：权值如果有非负 那么提前把非负的点提前选上
在二分图中，最大权独立集 = 所有点的总权值 - 最小权点覆盖&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;假设点集 $V$ ，点覆盖集为 $V_1$ ，补集 $V_2=V-V_1$ 是独立集
&lt;strong&gt;证明&lt;/strong&gt;
反证法：假设 $V_2$ 中 $v_1,v_2$ 有边，说明这条边里面没有被覆盖，因此与假设矛盾&lt;/li&gt;
&lt;li&gt;假设有独立集 $V_2$ ，$V_1 = V - V_2$ 一定是点覆盖集
&lt;strong&gt;证明&lt;/strong&gt;
反证法：假设 $V_1$ 不是点覆盖集，那么一定存在一条边 $(v_1,v_2)$ 没有被覆盖，或者说任意两点不在 $V_1$ 中，因此其都在 $V_2$ 中，与 $V_2$ 是独立集矛盾&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;做题思路&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;构造流网络&lt;/li&gt;
&lt;li&gt;证明简单割和原图可行方案一一对应&lt;/li&gt;
&lt;li&gt;思考数量关系&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;费用流模型&lt;/h1&gt;
&lt;p&gt;费用流--所有最大可行流中费用最小/最大值：$\sum f(u,v)w(u,v)$&lt;/p&gt;
&lt;p&gt;Q：图 $G$ 有最大流，是否一定有最小费用最大流？A：一定有&lt;/p&gt;
&lt;p&gt;把 EK 算法中的 bfs 换成 spfa 即可求出费用流
一般情况下，要保证没有负权回路&lt;br /&gt;
假设正向边的费用是 $w(u,v)$，那么反向边的费用为 $-w(v,u)$ 只要原图没有负权回路，那么残留网络求最短路就没有负权回路&lt;/p&gt;
&lt;p&gt;如果有负环回路，需要消圈法去解决&lt;/p&gt;
&lt;h2&gt;二分图最优匹配&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;敬请期待&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;无源汇有上下界最小费用可行流&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;敬请期待&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>图论小知识点</title><link>https://lzleureka.cn/posts/graphtheory/%E5%9B%BE%E8%AE%BA%E5%B0%8F%E7%9F%A5%E8%AF%86%E7%82%B9/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/%E5%9B%BE%E8%AE%BA%E5%B0%8F%E7%9F%A5%E8%AF%86%E7%82%B9/</guid><description>比较小众的图论知识点</description><pubDate>Mon, 15 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;欧拉路径与欧拉回路&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;注意：所有边连通且可以存在孤立点&lt;/strong&gt;
&lt;strong&gt;在无向图中，&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个图&lt;strong&gt;存在欧拉路径&lt;/strong&gt;的充分必要条件：度数为奇数的点只能有 0 个或者两个&lt;/li&gt;
&lt;li&gt;一个图&lt;strong&gt;存在欧拉回路&lt;/strong&gt;的充分必要条件：度数为奇数的点只能为 0 个&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;在有向图中，&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个图&lt;strong&gt;存在欧拉路径&lt;/strong&gt;的充分必要条件：要么所有点的入度等于出度，要么除了两个点之外，其余所有点的出度等于入度，剩余的两个点：一个满足于入度比出度多1（终点），一个满足出度比入度多1（起点）。&lt;/li&gt;
&lt;li&gt;一个图&lt;strong&gt;存在欧拉回路&lt;/strong&gt;的充分必要条件：所有点的入度等于出度&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;证明&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;必要条件：
无论是有向图还是无向图的必要条件证明都是从以下思路出发，首先对于图中的一笔画问题，肯定是有一个起点和终点，那么除了起点和终点，其余所有点都需要先进来再出去，然后对于起点来说可以没有过来的点，对于终点来说可以没有可以去的点。对于回路来说，就是所有点都需要进去和出去。&lt;/p&gt;
&lt;p&gt;充分条件：
通过给出一种构造方案来进行证明&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;构造方案：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dfs(u) {
	for  u 的所有边
		 dfs() // 扩展
	seq &amp;lt;- u // u 加入到路径序列中
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此构造方案为欧拉路径的逆序，正如下图中的 $(b)$，首先从一个奇数点（起点，如果奇数点为0，则任意点）一直往后遍历，由于中间点的度数是偶数，因此遍历到中间点一定可以再去下一条边，直到回到起点（如果是欧拉回路，一定是起点），或者某个点的度为奇数（终点）。然后就是回溯，在回溯的过程中保存欧拉回路/路径的点，然后如果碰到了没有遍历完的点，则继续遍历，因此此时已经没有终点，所以最终一定会回到这个点，这样就相当于在此处有一个环，也可以通过逆序的方式将其欧拉路径融合起来。从而所得欧拉路径就是一个终点到起点的欧拉路径逆序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;
使用邻接表存储时，假设图是一个有 $m$ 条边的自环，正如下图中的$(a)$，事实上，时间复杂度是 $O(m^2)$ 的，如果是邻接矩阵就是 $O(nm)$ 的时间复杂度。因此，在 $m$ 比较大的时候，可以选择直接删除一条边来降低&lt;strong&gt;时间复杂度为 $O(n+m)$&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;事实上，在随机数据上，这个时间复杂度还是挺快的。&lt;/p&gt;
&lt;p&gt;另外，在删边方面，有向图删边直接就删除了，但是在无向图里面，由于是加入了正反两条边，因此，根据链式前向星的特性，$i$ 所对应的反边在 $i \oplus 1$
&lt;img src=&quot;../images/Euler_path.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;欧拉回路&lt;/h2&gt;
&lt;h3&gt;判断欧拉回路&lt;/h3&gt;
&lt;p&gt;有向图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在添加边的时候注意计算入度和出度即可
for (int i = 1; i &amp;lt;= n; i ++ )
    if (din[i] != dout[i]) {
        puts(&quot;NO&quot;);
        return 0;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无向图&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在添加边的时候同样可以计算入度和出度，而且只需要计算一次即可
// 因此一条边要么是作入度要么作出度
// 相较于某个点来说，只需要作入度和作出度的总共次数为偶数即可
// 因为我们没必要在建立反向边的时候也更新入度和出度
// 否则其判断方式为 din[i] &amp;amp; 1
// 如果只考虑无向边，则计算度然后用度判断即可
for (int i = 1; i &amp;lt;= n; i ++ )
    if (din[i] + dout[i] &amp;amp; 1) { // 或者 d[i] &amp;amp; 1
        puts(&quot;NO&quot;);
        return 0;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;求欧拉回路方案&lt;/h3&gt;
&lt;p&gt;注意：如果是求&lt;strong&gt;欧拉路径&lt;/strong&gt;，对于有向图来说，初始点一定是出度数-入度数=1的点；对于无向图来说，初始点一定是度数为奇数两个点中的一个点。&lt;/p&gt;
&lt;h4&gt;边方案&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;边标记&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
这里只是把用过的边进行标记，但是并没有删除
对于上述说的情形，事实上这个这个复杂度是 O(m^2)
*/
void dfs(int u) {
    
    for (int i = h[u]; ~i; i = ne[i]) {

        if (used[i]) continue;
        used[i] = true;
        // 如果不是无向图，下面这行删掉
        if (type == 1) used[i ^ 1] = true; 
        dfs(e[i]);
        
        int t = i + 1;
        if (type == 1) {
            t = i / 2 + 1;
            if (i &amp;amp; 1) t = -t;
        }
        path[++ cnt] = t;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;删边优化&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
注意在for循环中，后面是 i = h[u]，表示每次只使用当前可用的边
如果不加入此优化，复杂度最坏可为 O(m^2)
*/
void dfs(int u) {
	
    for (int i = h[u]; ~i; i = h[u]) {

        h[u] = ne[i];
        if (used[i]) continue;
        if (type == 1) used[i ^ 1] = true;

        dfs(e[i]);

        int t = i + 1;
        if (type == 1) {
            t = i / 2 + 1;
            if (i &amp;amp; 1) t = -t;
        }
        path[++ cnt] = t;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;点方案&lt;/h4&gt;
&lt;h1&gt;2-SAT&lt;/h1&gt;
&lt;h2&gt;建图&lt;/h2&gt;
&lt;p&gt;对于任意一个布尔变量，拆成两个点，分别为 $x_i$ 和 $\neg x_i$
若给定一个 $x_i \vee x_j$ ，建造两条边，分别为 $\neg x_i \rightarrow x_j$ 和 $\neg x_j \rightarrow x_i$ ，如果 $x_i,\neg x_i$ 出现同一个强连通分量里面，说明这个逻辑表达式有矛盾，如果没有遍历每个点，对于 $x_i,\neg x_i$ 来说，谁的 &lt;code&gt;id&lt;/code&gt; 值越小，就取值哪个&lt;/p&gt;
&lt;p&gt;但是题目不一定是给出 $x_i \vee x_j$ ，有可能是 $a\rightarrow b$ 或者 $a=1/0$ ，对于 $a = 1$ ，可以转化成 $a \vee a$&lt;/p&gt;
</content:encoded></item><item><title>树与图的存储</title><link>https://lzleureka.cn/posts/graphtheory/%E6%A0%91%E4%B8%8E%E5%9B%BE%E7%9A%84%E5%AD%98%E5%82%A8/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/%E6%A0%91%E4%B8%8E%E5%9B%BE%E7%9A%84%E5%AD%98%E5%82%A8/</guid><description>树与图存储的小妙招</description><pubDate>Sun, 14 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;树与图的存储&lt;/h1&gt;
&lt;p&gt;树是一种特殊的图，与图的存储方式相同。
对于无向图中的边(a,b)，存储两条有向边$a\rightarrow b, b \rightarrow a$。
因此我们可以只考虑有向图的存储。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;邻接矩阵
&lt;code&gt;g[a][b]&lt;/code&gt; 存储边$a\rightarrow b$&lt;/li&gt;
&lt;li&gt;邻接表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 对于每个点k，开一个单链表，存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 另外可以使用栈把所有可开辟空间存储起来，然后每次 add 的时候就从栈顶取空间，删除则把空间加入栈中 一般来说只有大量删除的时候才使用栈式的空间开辟

// 添加一条边a-&amp;gt;b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;链式前向星的作用&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; 的反向边是 &lt;code&gt;i ^ 1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;有向图中 $i$ 是表示第 $i + 1$ 条插入的边，无向图中 $i$ 是第 $i / 2 + 1$ 条插入的边&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>连通分量</title><link>https://lzleureka.cn/posts/graphtheory/connected-component/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/connected-component/</guid><description>关于图论中连通分量知识与代码模板总结</description><pubDate>Thu, 11 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;有向图的强连通分量（SCC）&lt;/h1&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;p&gt;有向图的连通分量：对于分量中任意两点 $u,v$ ，必然可以从 $u$ 走到 $v$，且从 $v$ 走到 $u$&lt;/p&gt;
&lt;p&gt;强连通分量：极大连通分量，也就是在这个连通分量里面任意加入一个点都不满足连通分量的定义&lt;/p&gt;
&lt;p&gt;有向图的强连通分量的作用：将有向图的强连通分量进行缩点，进而转化成有向无环图（DAG），也就是拓扑图，方便使用递推求最短/长路&lt;/p&gt;
&lt;p&gt;注意，以下所表示的双亲或者孩子都是从 &lt;code&gt;dfs&lt;/code&gt; 序树来说明
树枝边：如下图所示，$dfn[u] = dfn[v]+1$，说明 $v \rightarrow u$ 是树枝边，这意味着 &lt;code&gt;dfs&lt;/code&gt; 过程中，遍历了点 $v$ 之后就立马遍历 $u$ ，如下图绿色的边&lt;/p&gt;
&lt;p&gt;前向边：如下图所示，假设 $dfn[u]=dfn[v]+1$，$u^\prime$ 是 $u$ 的后代，那么 $v \rightarrow u^\prime$是前向边，正如图中紫色的点，这也意味着这条边是在回溯的时候遍历的&lt;/p&gt;
&lt;p&gt;后向边：如下图所示，假设 $v^\prime$ 是 $v$ 的后代，则 $v^\prime \rightarrow v$ 是后向边，正如图中青色的边&lt;/p&gt;
&lt;p&gt;横叉边：如下图所示，当 $u$ 不是 $v$ 的孩子且 $dfn[u] &amp;lt; dfn[v]$ 时，说明有一条 $v\rightarrow u$ 的横叉边，正如图中红色边所示
&lt;img src=&quot;../images/SCC.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;求解强连通分量&lt;/strong&gt;
由于我们从某个点出发，不一定能够一直遍历所有点，因此要遍历所有结点，才能够完整地求该图的强连通分量。另外，从某个点开始求各个强连通分量，这里的所有点，一定是不重不漏地属于某个强连通分量。&lt;/p&gt;
&lt;p&gt;从某个点出发，那么其所在的强连通分量的示意图一定是上图所示。在 &lt;code&gt;dfs&lt;/code&gt; 的过程中，$dfn[u]=low[u]$ 的点一定是强连通分量在 &lt;code&gt;dfs&lt;/code&gt; 序树上的根。&lt;/p&gt;
&lt;p&gt;其中 $dfn[u]$ 表示点 $u$ 的时间戳，$low[u]$ 表示从$u$能够遍历到没有在某个强连通分量的时间戳的最小值。&lt;/p&gt;
&lt;p&gt;那么，为什么 $dfn[u]=low[u]$ 是判断出某个强连通分量的重要依据？
首先，先来看算法特性，在求强连通分量的过程中，我们假设是有一个递归最后的强连通分量，那么根据时间戳来说，我们最后回溯到 $u$ 的时候，其后面遍历的所有点一定是强连通分量里面的所有点，因此，每当遍历一个新点的时候，我们选择使用栈来保存每个点&lt;/p&gt;
&lt;p&gt;然后就是正常的 &lt;code&gt;dfs&lt;/code&gt; 来遍历每个点，如果我们可以往下遍历，那么其孩子能够到的最小时间戳，也是当前点能到的时间戳，如果是可以走到一个已经遍历的点，那么这个点有着一个有效或者无效的概念，区别在于是否在栈中，如果是前向边，有效或者无效没有意义，因为 $low$ 值不会更新；如果是后向边，首先这个点一定是在栈中，然后同时其 $low$ 值也可更新；如果是横叉边，那么如果所对应的点不在栈中，说明这个点已经作为某个强连通分量中的点出栈了，如果在栈中，则说明其 $low$ 值一定可以更新。因为横叉过去的那个点$v$其不会出栈的理由在于 $low[v] \lt dfn[v]$ ，同理 $low[v]$ 这个点没有出栈的理由也是在于 $low[low[v]] &amp;lt; dfn[low[v]]$，如此往上，定会走到 $(u,v)$ 的一个公共祖先，因此可以发现 $u \rightarrow v$ ，也可以 $v \rightarrow u$ 。&lt;strong&gt;综合起来就是如果判断的是时间戳比较小的，如果它在栈中，就可以更新 $low$ 值&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;从而，无法更新也就意味着这个强连通分量的 &lt;code&gt;dfs&lt;/code&gt; 序根找到了&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 
    // 求强连通分量
    for (int i = 1; i &amp;lt;= n; i ++ )
        if (!dfn[i])
            tarjan(i);
*/
void tarjan(int u) {
    dfn[u] = low[u] = ++ timestamp;
    stk[top ++ ] = u, in_stk[u] = true;
    
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (in_stk[v]) low[u] = min(low[u], dfn[v]);
    }
    
    if (low[u] == dfn[u]) {
        ++ scc_cnt;
        int p;
        do {
            p = stk[ -- top];
            in_stk[p] = false;
            id[p] = scc_cnt;
        } while (p != u);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;拓展&lt;/h2&gt;
&lt;p&gt;使得任意一个图变成强连通分量需要加的最少边数？
首先对这个图进行 tarjan 缩点&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果这个图本身是强连通分量，那么就不需要加&lt;/li&gt;
&lt;li&gt;如果这个图不是强连通分量，那么对其缩点后一定是多个拓扑图，假设入度为 0 的点的数量为 $din$，出度为 0 的点的数量为 $dout$ ，那么最少需要加入 $max(din, dout)$ 条边就能变成强连通图，正如下图所示&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../images/scc_case1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;无向图的双连通分量（DCC）&lt;/h1&gt;
&lt;p&gt;双连通分量分为点双连通分量（&lt;code&gt;v-DCC&lt;/code&gt;）和边双连通分量（&lt;code&gt;e-DCC&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;无向图的边双连通分量的作用：将无向图的边双连通分量进行缩点，进而转化成一棵树&lt;/p&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;p&gt;桥：桥为无向图中的一类特殊的边，删除这条边之后，图不再连通，我们称这样的边为&lt;strong&gt;桥&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;边双连通分量：&lt;strong&gt;极大的&lt;/strong&gt;不含有桥的连通块就称这个连通块为&lt;strong&gt;边连通分量&lt;/strong&gt;，换句话说，这样的图里面任意两点之间都有两条不相交的路径（点可以相交）&lt;/p&gt;
&lt;p&gt;割点：如果在图中把某个点以及它相关的边删掉，整个图不连通了，就称这个点为这个图的一个割点&lt;/p&gt;
&lt;p&gt;点双连通分量：&lt;strong&gt;极大的&lt;/strong&gt;不包含割点的连通块，我们称为点双连通分量&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;每个割点至少属于两个连通分量&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两个割点之间的边不一定是桥，一个桥之间的两个端点也不一定是割点，割点和桥之间并没有什么关系&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;点双连通分量不一定是边双连通分量，边双连通分量不一定是点双连通分量&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;求解&lt;/h2&gt;
&lt;h3&gt;边双连通分量&lt;/h3&gt;
&lt;p&gt;求解边双连通分量事实上和求有向图的强连通分量思路差不多，要注意的两点就是&lt;strong&gt;找桥&lt;/strong&gt;和由于链式前向星的建图特性，&lt;strong&gt;我们不能通过建立的反向边往上遍历，或者说更新 $low[u]$&lt;/strong&gt; ，因为本质上这是一条边。&lt;/p&gt;
&lt;p&gt;如何找桥？当遍历到某个，然后我们不能回到这个点的父亲结点的时候，说明这两个点连接的边是桥，从父亲角度来说，也就是 $low[u] &amp;lt; low[v]$ 的时候，当前这条边是桥&lt;/p&gt;
&lt;p&gt;如果判断反向边？如果之前遍历的边的下标为 $from$，那么这里就不能通过 $from \oplus 1$ 来遍历或者说更新 $low[u]$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：由于无向图中不会存在横叉边，所以 in_stk 数组本质上是可以省略的&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int h[N], e[M &amp;lt;&amp;lt; 1], ne[M &amp;lt;&amp;lt; 1], idx = 0;
int n, m;
vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; bridge;
int d[N];
int dfn[N], low[N], timestamp = 0;
int stk[N], tt = 0;
int id[N], dcc_cnt = 0;

void tarjan(int u, int from) {
    dfn[u] = low[u] = ++ timestamp;
    stk[tt ++ ] = u;
    
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan(v, i);
            low[u] = min(low[u], low[v]);
            if (low[u] &amp;lt; low[v]) {
                bridge.push_back({u, v});
                // 这里可以把图保存下来，也可以标记边
            }
        } else if (i != (from ^ 1)) low[u] = min(low[u], dfn[v]);
    }
    
    if (dfn[u] == low[u]) {
        ++ dcc_cnt;
        int y;
        do {
            y = stk[-- tt];
            id[y] = dcc_cnt;
        } while (y != u);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：在使用 from^1 != i 判断的时候一定要保证同时建立双向边&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;点双连通分量&lt;/h3&gt;
&lt;h4&gt;求割点&lt;/h4&gt;
&lt;p&gt;对于一条边 $(x,y)$ ，有 $dfn[x] &amp;lt; dfn[y]$ ，对于 $low[y] \ge dfn[x]$， 有&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 $x$ 不是根节点，那么 $x$ 是割点&lt;/li&gt;
&lt;li&gt;如果 $x$ 是根节点，至少有两个子结点 $y_i$ 使得 $low[y_i] \ge dfn[x]$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
遍历注意设置全局根节点
for (root = 0; root &amp;lt; n; root ++ )
    if (!dfn[root]) 
        tarjan(root);
*/
int n, m, root;
int h[N], e[M], ne[M], idx = 0;
int dfn[N], low[N], timestamp = 0;
vector&amp;lt;int&amp;gt; point; // 存放割点

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void tarjan(int u) {
    dfn[u] = low[u] = ++ timestamp;
    
    int cnt = 0;
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan(v);
            if (dfn[u] &amp;lt;= low[v]) cnt ++ ;
            low[u] = min(low[u], low[v]);
        } else low[u] = min(low[u], dfn[v]);
    }
    if (cnt + (root != u) &amp;gt; 1) point.push_back(u)// u 是割点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;求点双连通分量&lt;/h4&gt;
&lt;p&gt;点双连通分量的缩点方案是：1. 割点单独作为一个点。 2. 从每个 V-DCC 向其所包含的每个割点连边
对其进行缩点之后，图中点的数量等于所有连通分量的数量+割点的数量&lt;/p&gt;
&lt;p&gt;注意：求点连通分量是要把割点存进去的，一个割点可能在多个不同的连通分量中。&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 预处理
        for (int i = 1; i &amp;lt;= dcc_cnt; i ++ ) dcc[i].clear();
        memset(dfn, 0, sizeof dfn);
        memset(h, -1, sizeof h);
        memset(cut, 0, sizeof cut);
        dcc_cnt = timestamp = tt = idx = 0;
*/  
/* 求点双连通分量
        for (root = 1; root &amp;lt;= n; root ++ )
            if (!dfn[root])
                tarjan(root);
*/                
/* 求每个点双连通分量里面有多少个割点
        for (int i = 1; i &amp;lt;= dcc_cnt; i ++ ) {
            int cnt = 0;
            for (auto &amp;amp;u: dcc[i])
                if (cut[u]) cnt ++ ;
        }
*/
int root;
int h[N], e[N], ne[N], idx = 0;
int dfn[N], low[N], timestamp;
vector&amp;lt;int&amp;gt; dcc[N];
int dcc_cnt;
int stk[N], tt = 0;
bool cut[N];

void tarjan(int u) {
    
    dfn[u] = low[u] = ++ timestamp;
    stk[tt ++ ] = u;
    
    if (root == u &amp;amp;&amp;amp; h[u] == -1) {
        
        ++ dcc_cnt;
        dcc[dcc_cnt].push_back(u);
        return ;
    }
    int cnt = 0;
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[v], low[u]);
            if (dfn[u] &amp;lt;= low[v]) {
                cnt ++ ;
                if (root != u || cnt &amp;gt; 1) cut[u] = true;
                ++ dcc_cnt;
                int y;
                do {
                    y = stk[-- tt];
                    dcc[dcc_cnt].push_back(y);
                } while (y != v);
                dcc[dcc_cnt].push_back(u);
            } 
        } else low[u] = min(low[u], dfn[v]);
    }
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>最近公共祖先</title><link>https://lzleureka.cn/posts/graphtheory/%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/</guid><description>各种最近公共祖先的求法</description><pubDate>Tue, 09 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;向上标记法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 第一次dfs标记根节点的该结点之间所有点
bool dfs(int cur, int fa, int val) {
    if (cur == val) {
        id[cur] = true;
        return true;
    }
    for (int i = h[cur]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue;
        if (dfs(v, cur, val)) {
            id[cur] = true;
            return true;
        }
    }
    return false;
}
// 第二次dfs遇到有标记的点就是lca(a,b)
int redfs(int cur, int fa, int val) {
    if (cur == val) {
        if (id[cur]) return cur;
        return -1;
    }
    for (int i = h[cur]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue;
        int t = redfs(v, cur, val);
        if (t &amp;gt; 0) return t;
        else if (t == -1) {
            if (id[cur]) return cur;
            return -1;
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;倍增法&lt;/h2&gt;
&lt;p&gt;预处理 $O(nlogn)$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$f[i,k]$：结点 $i$ 的第 $2^k$ 个祖先 $0 \le k \le logn$，其中 $n$ 是结点的数量，注意下标从 $1$ 开始，因为这样方便结点 $0$ 充当一个虚拟最大祖先，也就是哨兵的意思&lt;/li&gt;
&lt;li&gt;$depth[i]$ 表示结点 $i$ 的深度，注意 $depth[i]&amp;gt;0$，因为这样方便 $\forall i, depth[i] &amp;gt; depth[0]$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;根据预处理求结点 $x$ 的第 $k$ 个祖先
对 $k$ 进行二进制拆分：$k = 2^{a_1}+2^{a_2}+\cdots+2^{a_j}$ ，因此求 $x$ 的第 $k$ 个祖先就是 $f(f(f(x,a_1),a_2) \cdots , a_j)$&lt;/p&gt;
&lt;p&gt;求 $lca(x,y)$  $O(logn)$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对齐深度，把深度较高的点，假设为 $x$ ，找到 $x$ 的第 $depth[x]-depth[y]$ 个祖先结点 $x^\prime$ ，这样就对齐了两点的深度。&lt;/li&gt;
&lt;li&gt;同样地，利用二进制拆分将 $x^\prime,y$ 移动到 $lca(x,y)$ 的下一个结点，最终再将变化后的 $x^\prime$ 或 $y$ 再往上跳一格，也就是 $f[x^\prime][0]$ 就是最近公共祖先&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
dfs调用: dfs(r, 0, 1)
*/
int h[N], e[N &amp;lt;&amp;lt; 1], ne[N &amp;lt;&amp;lt; 1], idx = 0;
int f[N][18], dep[N];
int n, r, tot = 0;

void dfs(int u, int fa, int cur) {
    
    dep[u] = cur;
    f[u][0] = fa;
    for (int i = 1; i &amp;lt; 18; i ++ )
        f[u][i] = f[f[u][i - 1]][i - 1];
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue;
        dfs(v, u, cur + 1);
    }
}
// bfs 预处理更加保险，不会爆栈
void bfs() {
    queue&amp;lt;int&amp;gt; Q;
    Q.push(r);
    dep[r] = 1;
    st[r] = true;
    
    while (Q.size()) {
        int u = Q.front(); Q.pop();
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (st[v]) continue;
            dep[v] = dep[u] + 1;
            f[v][0] = u;
            for (int i = 1; i &amp;lt; 18; i ++ )
                f[v][i] = f[f[v][i - 1]][i - 1];
            Q.push(v);
            st[v] = true;
        }
    }
}

int lca(int x, int y) {
    
    if (dep[x] &amp;gt; dep[y]) swap(x, y);
    for (int i = 17; i &amp;gt;= 0; i -- )
        if (dep[f[y][i]] &amp;gt;= dep[x])
            y = f[y][i];
            
    if (x == y) return x;
    for (int i = 17; i &amp;gt;= 0; i -- )
        if (f[x][i] != f[y][i])
            x = f[x][i], y = f[y][i];
    return f[x][0];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;tarjan 离线算法&lt;/h2&gt;
&lt;h3&gt;离线算法和在线算法&lt;/h3&gt;
&lt;p&gt;离线算法：一般的面向有多次询问的题面，然后一次性把所有询问读入，然后再输出结果的算法
在线算法：即问即答&lt;/p&gt;
&lt;h3&gt;tarjan 求 lca&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../images/tarjan%E6%B1%82lca.png&quot; alt=&quot;&quot; /&gt;
如图所示：在 &lt;code&gt;dfs&lt;/code&gt; 的过程中访问到某点时我们可以将点分为三类，其中蓝色的点表示已经访问完的点，绿色的点相当于在递归栈中的点，也就是正在访问的点，白色的点表示未尚未标记的点。&lt;/p&gt;
&lt;p&gt;根据这样对点进行划分，有一个结论：如果要求两个点的最近公共祖先，其中一个点在已经访问完的点中，一个点就是当前正在访问的点，那么这些点对的最近公共祖先一定是绿色集合中的点。为此，可以提前将所有询问存储，进而设计一个一次遍历整棵树的同时回答所有询问的离线算法。&lt;/p&gt;
&lt;p&gt;算法流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先存储所有询问，存储方式类似于 &lt;code&gt;vector&lt;/code&gt; 写法的邻接表，&lt;code&gt;query[i]&lt;/code&gt; 存储所有与点 $i$ 相关的查询与其查询索引&lt;/li&gt;
&lt;li&gt;然后从任意一个点做 tarjan 算法
2.1 首先遍历当前点的所有孩子，然后遍历完利用并查集把当前孩子的双亲设置为当前结点
2.2 利用并查集求解与这个点所有相关的，另外一个点是已经访问完的所有询问d
2.3 将当前结点标记为已经访问完的状态
代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int h[N], e[M], ne[M], idx = 0;
int st[N], fa[N];
vector&amp;lt;PII&amp;gt; query[M];
int lca[M];
int n, m;

void tarjan(int u) {
    
    st[u] = 1;
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (st[v]) continue;
        tarjan(v);
        fa[v] = u;
    }
    for (auto [v, id]: query[u])
        if (st[v] == 2)
            lca[id] = get(v);
    st[u] = 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;RMQ 求 lca&lt;/h2&gt;
</content:encoded></item><item><title>组合数学</title><link>https://lzleureka.cn/posts/math/combinatorial-mathematics/</link><guid isPermaLink="true">https://lzleureka.cn/posts/math/combinatorial-mathematics/</guid><description>关于组合数学的各种总结</description><pubDate>Tue, 09 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;基础&lt;/h1&gt;
&lt;h2&gt;排列&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;从 $n$ 个不同的物品中不重复地取出 $r$ 个，排列数 $P_n^r = n!/(n-r)!$&lt;/li&gt;
&lt;li&gt;从 $n$ 个不同的物品中可重复地取出 $r$ 个，排列数为 $n^r$&lt;/li&gt;
&lt;li&gt;圆排列的排列数：从 $n$ 个元素中选 $r$ 个的圆排列的排列数为 $\frac{P_n^r}{r}=\frac{n!}{r(n-r)!}$&lt;/li&gt;
&lt;li&gt;$S$ 是一个多重集，有 $k$ 个不同的元素，每个元素都有无穷重复个数，那么 $S$ 的 $r$ 排列个数为 $k^r$&lt;/li&gt;
&lt;li&gt;$S$ 是一个多重集，有 $k$ 个不同的元素，每个元素的重数分别为 $n_1,\cdots,n_k$，$n=n_1+n_2+\cdots+n_k$，则 $S$ 的 $n$ 排列的个数为 $\frac{n!}{n_1!n_2! \cdots n_k!}$&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;组合&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;从 $n$ 个不同物品中选 $r$ 个元素的组合为 $C_n^r$&lt;/li&gt;
&lt;li&gt;$S$ 是一个多重集，有 $k$ 个不同的元素，每个元素都有无穷个，$S$ 的 $r$ 组合的个数为 $C_{r+k-1}^{k-1}$ 可以不定方程隔板法证明&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;求组合数&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;利用公式 $C_a^b = C_{a-1}^b + C_{a - 1}^{b-1}$
数据范围 $a,b \le 2000$ 左右
代码如下&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void init() {
    for (int i = 0; i &amp;lt; N; i ++ )
        for (int j = 0; j &amp;lt;= i; j ++ ) {
            if (!j) C[i][j] = 1;
            else C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % MOD;
        }
}	
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;利用公式 $C_a^b = \frac{a!}{b!(a-b)!}$ ，因此可以预处理阶乘和取除法的模所需逆元
数据范围大概在 $10^5$ 左右
&lt;strong&gt;注意&lt;/strong&gt;：模数得是素数
代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/*
使用：C_a^b = fact[a] * inv[b] * inv[a - b]
*/
int fact[N];
int inv[N];

int kpow(int a, int k, int m) {
    int res = 1;
    while (k) {
        if (k &amp;amp; 1) res = 1ll * res * a % m;
        k &amp;gt;&amp;gt;= 1;
        a = 1ll * a * a % m;
    }
    return res;
}

void init() {
    inv[0] = fact[0] = 1;
    for (int i = 1; i &amp;lt; N; i ++ )
        fact[i] = 1ll * fact[i - 1] * i % MOD, inv[i] = kpow(fact[i], MOD - 2, MOD);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Lucas 定理，$p$ 为素数，使用限制 $a \ge p$
时间复杂度 $O(n)$
$$
C_a^b \equiv C_{a \ mod \ p} ^ {b \ mod \ p} * C_{a / p} ^ {b / p} (mod \ p)
$$
可以证明：
先将 a, b 分别写成 p 进制
$$
a = a_0 + a_1p + a_2p^2 + \cdots + a_kp^k
$$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$$
b = b_0 + b_1p + b_2p^2 + \cdots + b_kp^k
$$
由于 $(1+x)^p=1 + C_p^1x + C_p^2x^2 + \cdots + C_p^px^p$，而 p 又是质数，因此 $(1+x)^p \equiv 1 + x^p (mod \ p)$
同理，也可知 $(1+x)^{p^k} \equiv 1 + x^{p^k} (mod \ p)$ ，其中 $k \ge 1$
可以利用生成函数证明，由于
$$
(1 + x)^a = (1+x)^{a_0 + a_1p + a_2p^2 + \cdots + a_kp^k} = (1 + x)^{a_0}((1+x)^p)^{a_1} \cdots ((1+x)^{p^k})^{a_k}
$$
故
$$
(1 + x)^a \equiv (1+x)^{a_0}(1+x^p)^{a_1}\cdots (1+x^{p^k})^{a_k} (mod \ p)
$$
左边关于 $x^b$ 的系数为 $C_a^b$ ，右边关于 $x_b$ 即 $x^{b_0 + b_1p + b_2p^2 + \cdots + b_kp^k}$ 为
$$
C_{a_0}^{b_0}C_{a_1}^{b_1}\cdots C_{a_k}^{b_k}
$$
由于 $b \ mod \ p = b_0, a \ mod \ p = a_0$ ，故 $C_{a_0}^{b_0} = C_{a \ mod \ p} ^ {b \ mod \ p}$
又 $a / p = a_1 + a_2p^1 + \cdots + a_kp^{k-1}$，$b / p = b_1 + b_2p^1 + \cdots + b_kp^{k-1}$
由上述求解 $C_a^b$ 的过程同理可得 $C_{a / p}^{b / p} = C_{a_1}^{b_1}\cdots C_{a_k}^{b_k}$
因此原命题得证&lt;/p&gt;
&lt;p&gt;计算利用lucas计算组合数步骤：（1）线性筛；（2）lucas递归计算；（3）利用质因数的指数求$C_a^b$，其中 $a,b&amp;lt;p$ ，特判 $a&amp;lt;b$ 时组合数为 0，求质因数的指数使用阶乘的质因数分解&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;高精度算 $C_a^b$ ，所需知识点：[[数论&amp;amp;线性代数#阶乘质因数分解]] ；然后利用高精度算乘法
时间复杂度 $n \le 30000$ ，差不多是比较极限
代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int h[N];
int p[N], cnt = 0;
bool st[N];
// 欧拉筛
void get_euler(int n) {
    
    for (int i = 2; i &amp;lt;= n; i ++ ) {
        if (!st[i]) p[cnt ++ ] = i;
        for (int j = 0; i &amp;lt;= n / p[j]; j ++ ) {
            st[i * p[j]] = true;
            if (i % p[j] == 0) break;
        }
    }
}
// 阶乘质因数分解
void divide(int n, int type) {
    
    for (int i = 0; p[i] &amp;lt;= n; i ++ ) {
        int sum = 0;
        for (int j = n / p[i]; j; j /= p[i]) sum += j;
        h[p[i]] += type * sum;
    }
} 

vector&amp;lt;int&amp;gt; mul(vector&amp;lt;int&amp;gt; &amp;amp;a, int b) {
    vector&amp;lt;int&amp;gt; c;
    for (int i = 0, t = 0; t || i &amp;lt; a.size(); i ++ ) {
        if (i &amp;lt; a.size()) t += b * a[i];
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组合计数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;递推法，也就是&lt;code&gt;dp&lt;/code&gt;状态设计&lt;/li&gt;
&lt;li&gt;隔板法
经典应用 $x_1+x_2+\cdots+x_k = n$ 有多少组正整数解，其中的等式可以变成不等式，然后如果 $x_i \ge 0$ ，则可以利用 $y_i=x_i+1,\sum{y_i}=n+k$ 的映射来计算&lt;/li&gt;
&lt;li&gt;抽屉法
通常和整数求余进行结合，把余数用抽屉来处理&lt;/li&gt;
&lt;li&gt;特殊数列，卡特兰数&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;容斥原理&lt;/h1&gt;
&lt;p&gt;容斥原理计算公式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;集合 $S$ 中不具有性质 $P_1,P_2,\cdots,P_n$ 的对象个数为
$$
|\overline{A_1} \cap \overline{A_2} \cap \cdots \cap \overline{A_n}|=|S|-\sum|\overline{A_i}| + \sum|\overline{A_i}\cap \overline{A_j}| + \cdots + (-1)^n|\overline{A_1}\cap \cdots  \overline{A_n}|
$$&lt;/li&gt;
&lt;li&gt;集合 $S$ 中至少具有性质 $P_1,P_2,\cdots,P_n$ 之一的对象个数为
$$
|A_1\cup A_2 \cup \cdots \cup A_n| = \sum|A_i| - \sum|A_i \cap A_j| + \cdots  (-1)^{n+1}|A_1\cap A_2 \cap \cdots A_n|
$$&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;卡特兰数 Catalan 数&lt;/h1&gt;
&lt;p&gt;Catalan 数是一个数列，定义为 $C_n = \frac{1}{n+1}C_{2n}^n$
卡特兰数：$1,1,2,5,14,42,132,429,1430,4862,16796$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算公式&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$C_n=\frac{1}{n+1}C_{2n}^n = C_{2n}^n - C_{2n}^{n+1}$
该计算公式从一个基本模型中推导：把 $n$ 个 $1$ 和 $n$ 个 $0$ 排成一行，使得这一行的任意前 $k$ 个数中的 $1$ 的数量总是大于等于 $0$ 的数量&lt;/li&gt;
&lt;li&gt;递推 $C_n=C_0C_{n-1}+C_1C_{n-2}+\cdots+C_{n-1}C_0$ ，$C_0=1$&lt;/li&gt;
&lt;li&gt;$C_n = \frac{4n-2}{n+1}C_{n-1}, C_0=1$
当 $n\le 500$ 时，可以使用公式 $2$，比较大就需要 $1，3$ 公式&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;棋盘问题&lt;/li&gt;
&lt;li&gt;括号问题&lt;/li&gt;
&lt;li&gt;出栈序列问题&lt;/li&gt;
&lt;li&gt;二叉树问题&lt;/li&gt;
&lt;li&gt;三角剖分问题
有 $n+1$ 条边的凸多边形区域，在内部插入不相交的对角线，把凸多边形划分为多个三角形&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;二叉树问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;$n$ 个点形成的所有二叉树共有多少叶子数量？&lt;/strong&gt;
解答：对于每棵有 $n$ 结点，有 $g(n)$ 个叶子的二叉树，将其叶子删掉，将会得到 $g(n)$ 个 $n-1$ 结点的二叉树。而一个 $n - 1$ 结点的二叉树有 $n$ 个位置可以插入叶子，因此每棵 $n-1$ 结点的树被得到了 $n$ 次，因此 $f(n-1)n=g(n) = C_{2n-2}^{n-1}$&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;博弈论&lt;/h1&gt;
&lt;h2&gt;Nim 游戏&lt;/h2&gt;
&lt;p&gt;Nim 游戏：当一堆石子的数量异或值为 0 时，则为必败态，也即异或值不为 0 时，为必胜态&lt;/p&gt;
&lt;p&gt;证明：前置：必胜态转移的必败态只需要某种取法能转移到必败态即可，而必胜态转移到必败态需要无论此时如何操作，下一步转移一定是必败态&lt;/p&gt;
&lt;p&gt;1、首先证明，必胜态一定存在一种方式能转移为必败态
设一堆石子的数量为 $a_1, a_2, \cdots, a_n$，由必胜态可知， $a_1 \oplus a_2 \oplus \cdots \oplus  a_n = k \neq 0$, 因此$k$ 一定有高位 1 在第 $bit$ 位，那么一定存在 $a_i$ 的 $bit$ 位为 1。可以证明，若 {$a_i$} 中不存在 $bit$ 为 1，那么这些数异或的值 $k$ 的 $bit$ 位也一定不为 1，矛盾。&lt;/p&gt;
&lt;p&gt;因此，我们可以将 $a_i^{\prime} = a_i \oplus k$ ，由于 $k$ 的 $bit$ 位以上的位数为 $0$，因此，$a_i^\prime &amp;lt; a_i$  ，即在第 $i$ 堆取走 $a_i - a_i^\prime$ 个石子，那么此时所有石子数量异或值为 $a_1 \oplus a_2 \oplus \cdots a_i^{\prime} \cdots \oplus  a_n = a_1 \oplus a_2 \oplus \cdots a_i\oplus k \cdots \oplus  a_n = k \oplus k = 0$，这样就由必胜态转移为必败态&lt;/p&gt;
&lt;p&gt;2、然后证明，必败态无论如何操作都会转移为必胜态，即证，无论从哪堆石子拿石子，一定会使得其异或值不为 0&lt;/p&gt;
&lt;p&gt;反证法：假设在 $a_i$ 中拿 $a_i - a_i^{\prime} \gt 0$ 个石子，有 $a_1 \oplus a_2 \oplus \cdots a_i^{\prime} \cdots \oplus  a_n = k^{\prime} = 0$ 那么 $k \oplus k^{\prime} = a_i \oplus a_i^{\prime} = 0$，也就是说 $a_i = a_i^{\prime}$，这与假设不符，因此这样就相当于没有拿石子，矛盾。&lt;/p&gt;
&lt;p&gt;证毕&lt;/p&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;h3&gt;公平组合游戏&lt;/h3&gt;
&lt;p&gt;若一个游戏满足：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由两名玩家交替行动；&lt;/li&gt;
&lt;li&gt;在游戏进程的任意时刻，可以执行的合法行动与轮到哪名玩家无关；&lt;/li&gt;
&lt;li&gt;不能行动的玩家判负；
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏，但城建的棋类游戏，比如围棋，就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子，胜负判定也比较复杂，不满足条件2和条件3。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;有向图游戏&lt;/h3&gt;
&lt;p&gt;给定一个有向无环图，图中有一个唯一的起点，在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动，每次可以移动一步，无法移动者判负。该游戏被称为有向图游戏。
&lt;strong&gt;任何一个公平组合游戏都可以转化为有向图游戏&lt;/strong&gt;。具体方法是，把每个局面看成图中的一个节点，并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。&lt;/p&gt;
&lt;h3&gt;Mex 运算&lt;/h3&gt;
&lt;p&gt;设S表示一个非负整数集合。定义$mex(S)$为求出不属于集合S的最小非负整数的运算，即：
$mex(S) = min{x}$, $x$属于自然数，且$x$不属于$S$
比如：$mex({0,1,2,4,5})=3, mex({1,4,5,6})=0$&lt;/p&gt;
&lt;h3&gt;SG 函数&lt;/h3&gt;
&lt;p&gt;在有向图游戏中，对于每个节点$x$，设从$x$出发共有$k$条有向边，分别到达节点$y_1, y_2, …, y_k$，定义$SG(x)$为$x$的后继节点$y_1, y_2, …, y_k$ 的$SG$函数值构成的集合再执行$mex(S)$运算的结果，即：
$$SG(x) = mex({SG(y_1), SG(y_2), …, SG(y_k)})$$
特别地，整个有向图游戏$G$的$SG$函数值被定义为有向图游戏起点$s$的$SG$函数值，即$SG(G) = SG(s)$。&lt;/p&gt;
&lt;h3&gt;有向图游戏的和&lt;/h3&gt;
&lt;p&gt;设$G_1, G_2, …, G_m$ 是$m$个有向图游戏。定义有向图游戏$G$，它的行动规则是任选某个有向图游戏$Gi$，并在$Gi$上行动一步。$G$被称为有向图游戏$G_1, G_2, …, G_m$的和。
有向图游戏的和的$SG$函数值等于它包含的各个子游戏$SG$函数值的异或和，即：
$$SG(G) = SG(G_1) \oplus SG(G_2) \oplus … \oplus SG(G_m)$$&lt;/p&gt;
&lt;h4&gt;定理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;有向图游戏的某个局面必胜，当且仅当该局面对应结点的$SG$函数值大于0&lt;/li&gt;
&lt;li&gt;有向图游戏的某个局面必败，当且仅当该局面对应结点的$SG$函数值等于0&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>二分图</title><link>https://lzleureka.cn/posts/graphtheory/bipartite-graph/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/bipartite-graph/</guid><description>关于二分图知识与代码模板总结</description><pubDate>Fri, 05 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;判断二分图&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;一个图是二分图当且仅当这个图可以被2-染色，也就是图中不含奇数环&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;时间复杂度：$O(n)$&lt;/p&gt;
&lt;p&gt;特点：无向图，图中可能存在重边和自环。&lt;/p&gt;
&lt;p&gt;算法流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从前往后遍历每个结点，如果有结点未染色，则对其进行 dfs 判断
1.1 如果dfs判断其有奇数环，则说明该图不是二分图
&lt;em&gt;dfs部分：参数 ：当前结点，当前结点颜色&lt;/em&gt;
&lt;ol&gt;
&lt;li&gt;遍历与当前结点相连的结点
1.1 如果未被染色，则对其进行dfs染色，如果对其染色后存在奇数环，返回false
1.2 如果进行了然后，则判断当前结点和相连的结点的颜色是否一致，如果是则说明存在奇数环，返回false
1.3 如果上述情况都没有返回false，则返回true
代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int h[N], e[N &amp;lt;&amp;lt; 1], ne[N &amp;lt;&amp;lt; 1], idx = 0;
int st[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// dfs 判断
bool dfs(int u, int c) {
    
    st[u] = c;
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!st[v]) {
            if (!dfs(v, 3 - c)) return false;
        } else if (st[v] == st[u]) return false;
    }
    return true;
}
// 有可能这个图不连通，所以需要每个都遍历
bool flag = true;
for (int i = 1; i &amp;lt;= n; i ++ )
    if (!st[i] &amp;amp;&amp;amp; !dfs(i, 1)) {
        flag = false;
        break;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;二分图最大匹配&lt;/h1&gt;
&lt;h2&gt;匈牙利算法&lt;/h2&gt;
&lt;p&gt;时间复杂度：$O(nm)$&lt;/p&gt;
&lt;p&gt;特点：二分图，其中左半部包含 $n_1$个点（编号 1∼$n_1$），右半部包含 $n_2$ 个点（编号 1∼$n_2$），二分图共包含 $m$ 条边。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;交替路&lt;/strong&gt;：未匹配边$\rightarrow$匹配边$\rightarrow \cdots$ $\rightarrow$ 未匹配边 这样的交替路径
&lt;strong&gt;增广路径&lt;/strong&gt;：交替路的两端端点是未匹配点&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二分图的最大匹配等价于当前匹配无增广路径&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此，我们的算法策略就是不断地寻找增广路径，算法流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于某个未匹配的点$v$来说，遍历其所有相邻点 $v$
1.1 如果这个点没有被遍历过
2.1 如果这个点没有被匹配，则 $match[v] = u$，返回true
2.2 如果这个点有被匹配，那么继续去判断 $match[v]$ 开始的增广路径，如果找到了，也是将 $match[v] = u$ ，返回true&lt;/li&gt;
&lt;li&gt;如果上述操作未能找到增广路径，则返回 false&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在寻找增广路径的过程中，点遍历过了设置为 true，是否对后续没影响？是的，首先在寻找的过程中不应该找遍历过的点，然后如果当前点确实没找到增广路径，说明此处的匹配只能如此匹配，因此后边再遍历它也没有效果&lt;/li&gt;
&lt;li&gt;找每个点的匹配时，要注意初始化 st 访问状态&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int N = 100010;

int h[N], e[N], ne[N], idx = 0;
int match[N];
bool st[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool find(int u) {
    
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!st[v]) {
            st[v] = true;
            if (!match[v] || find(match[v])) {
                match[v] = u;
                return true;
            }
        }
    }
    return false;
}

/*
寻找最大匹配
int res = 0;
for (int i = 1; i &amp;lt;= n1; i ++ ) {
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}
/*
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;二分图匹配的必须边和可行边&lt;/h3&gt;
&lt;h4&gt;完备匹配&lt;/h4&gt;
&lt;p&gt;完备匹配指的是二分图里的任意一个顶点都是匹配边相关联&lt;/p&gt;
&lt;p&gt;那么，在完备匹配中，一条边 $(x,y)$ 是&lt;strong&gt;必须边&lt;/strong&gt;，当且仅当同时满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在当前这个完备匹配下，$(x,y)$ 是匹配边&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在当前这个完备匹配下，去掉 $(x,y)$ 这条边之后，不能再找一条增广路，其起点是 $x$，终点是 $y$&lt;/p&gt;
&lt;p&gt;因此，对于完备匹配求必须边有个暴力的做法，就是先求一遍匈牙利算法得到完备匹配，然后枚举每一个匹配，将其删掉，再做一次求增广路，看是否能找到一条增广路，找不到说明这条边是必须边&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在完备匹配中，一条边 $(x,y)$ 是可行边，当且仅当满足以下两个条件之一：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$(x,y)$当前是匹配边&lt;/li&gt;
&lt;li&gt;$(x,y)$ 是非匹配边，设当前 $x$ 与 $u$ 匹配， $y$ 与 $v$ 匹配，连接边 $(x,y)$ 后，结点 $u,v$ 失配，因此必须找到一条从 $u$ 到 $v$ 的增广路，来保证最大匹配不变&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么，在一个完备匹配中判定所有的必须边和可行边有一个通用算法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构造新的有向图：把&lt;strong&gt;非匹配边&lt;/strong&gt;看作左部到右部的有向边，匹配边看作从右部到左部的有向边来构造新图。&lt;/li&gt;
&lt;li&gt;求新图的强连通分量&lt;/li&gt;
&lt;li&gt;$(x,y)$ 是原图的必须边，说明 $x,y$ 当前是匹配边，并且 $x,y$ 在新图中属于不同的强连通分量&lt;/li&gt;
&lt;li&gt;$(x,y)$ 是原图的可行边，$(x,y)$ 当前匹配边或者 $x,y$ 两点在新图中属于相同的强连通分量&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;一般最大匹配&lt;/h4&gt;
&lt;p&gt;那么，事与愿违，一般情况下，二分图的最大匹配大多是非完备匹配，那么这种情况下就需要借助 [[网络流]]&lt;/p&gt;
&lt;p&gt;若 $z$ 是当前右部非匹配点，$(u,v)$ 是匹配边，则 $(z,T)$ 的容量为 $1$，$(v,T)$ 的反向边的容量一定是 $1$。因此，如果二分图中 $u$ 到 $z$ 有增广路，那么其残留网络上 $u$ 能到达 $z$ ，进而到达 $v$，即 $(u,v)$ 在一个强连通分量当中&lt;/p&gt;
&lt;p&gt;因此，必须边的判定条件：$(x,y)$ 流量为 $1$，并且 $x, y$ 两点在残留网络中属于不同的强连通分量
可行边的判定条件：$(x,y)$ 流量为 $1$，或者 $x,y$ 两点才残留网络中属于相同的强连通分量&lt;/p&gt;
&lt;h1&gt;最小点覆盖、最大独立集、最小路径点覆盖（最小路径重复点覆盖）&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;最大匹配数=最小点覆盖=总点数-最大独立集=总点数-最小路径覆盖&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;最小点覆盖&lt;/h2&gt;
&lt;p&gt;最小点覆盖：给你一个&lt;strong&gt;任意图&lt;/strong&gt;，从中选出最少的点，使得每条边至少有一个端点是被选出来的。&lt;/p&gt;
&lt;p&gt;![[]] 这里要加图&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;在二分图中&lt;/strong&gt;，最小点覆盖是等于最大匹配数的&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;证明&lt;/strong&gt;：
设 $A$ 为最小点覆盖，$B$ 为最大匹配数，因此等价于证明 $A=B$
先证明 $A \ge B$
由于二分图中最大匹配数里面的边都是没有交点的，因此要想覆盖所有边，必然匹配里面所有点都至少要选上，因此 $A \ge B$&lt;/p&gt;
&lt;p&gt;然后证明这个等号是可以成立的，通过&lt;strong&gt;构造&lt;/strong&gt;方案来证明&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求最大匹配&lt;/li&gt;
&lt;li&gt;从左部每个非匹配点出发，做一遍增广标记所有经过的点
左部所有未被标记的点  +   右部所有被标记的点 就是一个最小点覆盖方案
首先证明这个和是为最大匹配数，假设为 $m$&lt;/li&gt;
&lt;li&gt;左部所有非匹配点一定被标记&lt;/li&gt;
&lt;li&gt;右部所有非匹配点一定未被标记（否则为增广路，与最大匹配矛盾）、&lt;/li&gt;
&lt;li&gt;对于匹配边，左右两点要么同时标记要么同时不被标记
根据这三个性质，左部所有未被标记的点一定是匹配点，右部所有被标记的点也一定是匹配点，右部的匹配点可以分为有标记的和未被标记的，再根据第三条性质，选择点的数量一定是最大匹配数，同时也证明了匹配边一定被这些点覆盖。
剩下的就是要证明&lt;/li&gt;
&lt;li&gt;左部非匹配点  ----  右部匹配点连接的边 是否被覆盖&lt;/li&gt;
&lt;li&gt;左部匹配点     ----  右部非匹配点连接的边 是否被覆盖&lt;/li&gt;
&lt;li&gt;左部非匹配点  ----  右部非匹配点连接的边 是否被覆盖
第三种情况与最大匹配数矛盾，因此不存在这种情况。
第一种情况，由于我们是从左部非匹配点出发，那么和右部匹配点连接的边一定是被标记的，而由于我们选择了右部所有被标记的点，因此这样的边能被覆盖。
第二种情况，由于又不非匹配点一定是未被标记的，因此其连接的左部匹配点一定也未被标记，因为标记的过程从未匹配点出发，最后也能够到非匹配点，这样的话会有增广路径，这与最大匹配不符。因此，这样的左部匹配点一定是未被标记的，由于左部选择的是未被标记的匹配点，因此，这样的边也能覆盖。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上：&lt;strong&gt;二分图中，最小点覆盖=最大匹配数&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;最大独立集&lt;/h2&gt;
&lt;p&gt;最大独立集：从一个&lt;strong&gt;任意图&lt;/strong&gt;中，选出最多的点，使得选出的点之间没有边
最大团：从一个&lt;strong&gt;任意图&lt;/strong&gt;中，选出最多的点，任意两点之间都有边
补图：在图论中，一个图的补图是一个图跟图 $G$ 有相同的点，而且这些点之间有边相连当且仅当在 $G$ 里面他们没有边相连。
因此，独立集的补图就是团&lt;/p&gt;
&lt;p&gt;在&lt;strong&gt;二分图中&lt;/strong&gt;，求最大独立集$\iff$ 去掉最少的点将所有边都破坏掉 $\iff$ 找最小点覆盖 $\iff$ 找最大匹配数&lt;/p&gt;
&lt;h2&gt;最小路径点覆盖&lt;/h2&gt;
&lt;p&gt;最小路径点覆盖：简称最小路径覆盖，针对有向无环图来说，用最少的互不相交的路径（路径里点不重复）将所有点覆盖，求最少路径数&lt;/p&gt;
&lt;p&gt;思路：拆点
假设原图中有 $1-n$个点，设置 $1^\prime - n^\prime$ 个新点，$(i,j)$ 的边变成$(i, j^\prime)$，这样整个图就变成了一个二分图。
最后，原图最小路径覆盖就是 $n - m$，其中 $m$ 为新图的最大匹配数。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;证明&lt;/strong&gt;
首先来看在原图中的每条路径，转化到新图中是怎么样子的，比如：$a-&amp;gt;b-&amp;gt;c$ 。由于在一个路径点覆盖的方案当中，每个点都只出现一次，然后一定每个点都出现，因此，路径上的每个点在新图中每个点最多一个入度和一个出度。对于只有出度没有入度的点，说明是路径起点；只有入度没有出度的点，说明是路径终点，因此下面的等价关系正确。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;原图中的路径 $\iff$ 新图匹配边&lt;/li&gt;
&lt;li&gt;原图中的路径终点 $\iff$ 新图左部非匹配点
因此，求最少路径数也可以说是求最少路径终点数 $\iff$ 求最少左部非匹配点数 $\iff$ 求左部最大匹配点数 $\iff$ 找最大匹配数&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;最小路径重复点覆盖&lt;/h3&gt;
&lt;p&gt;如果在最小路径点覆盖的基础上放开限制，也即是说点和边是可以相交的，那么将所有点覆盖的路径数是多少？&lt;/p&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求传递闭包 $G^\prime$&lt;/li&gt;
&lt;li&gt;在 $G^\prime$ 上求最小路径覆盖
证明思路就是两个集合是等价的，或者说可以转化
原图所有的最小路径重复点覆盖集合  $\iff$ 新图所有最小路径点覆盖集合&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于任意的最小路径重复点覆盖集合，这样的集合一定是多条路径组合成，第一条路径不考虑，如果第 $i(i&amp;gt;1)$ 条路径上有之前重复的点，我们就将其跳过，如果是起点重复，起点就不考虑，&lt;strong&gt;不可能有一条路径所有点都被遍历过，否则这条路径有不有都无所谓，也不满足最小的限制&lt;/strong&gt;。因此，对于原图中的任意最小路径重复点覆盖集合，都可以变成新图中最小路径覆盖集合。&lt;/p&gt;
&lt;p&gt;而对于新图中所有最小路径点覆盖集合，也一定是有多条路径存在，那么可以查看路径中每条边，如果这条边在原图中并不存在，则还原成原图中的路径即可，因为是传递闭包，一定至少有一条路径，由于新图的最小路径点覆盖集合本质上已经覆盖了所有点，因此，对于还原的新边也只会出现重复出现的点，从而是满足要求的。&lt;/p&gt;
&lt;h4&gt;应用&lt;/h4&gt;
&lt;p&gt;DAG 上的一个点集，任意两点之间无路径，求这个点集最多的点的数量&lt;/p&gt;
</content:encoded></item><item><title>生成树模型</title><link>https://lzleureka.cn/posts/graphtheory/%E7%94%9F%E6%88%90%E6%A0%91%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/%E7%94%9F%E6%88%90%E6%A0%91%E6%A8%A1%E5%9E%8B/</guid><description>生成树模型相关算法，以及生成树计数相关算法 ~ </description><pubDate>Fri, 05 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;prim 算法&lt;/h1&gt;
&lt;p&gt;时间复杂度：$O(n^2)$&lt;/p&gt;
&lt;p&gt;特点：无向图，图中可能存在重边和自环，边权可能为负数。&lt;/p&gt;
&lt;p&gt;假设已选择的点在集合 $V_1$，尚未选择的点在集合 $V_2$
算法流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 $V_2$ 中任意选择一个点 $v$，放入 $V_1$ ，然后更新集合 $V_1$ 到集合 $V_2$ 里面所有点的最小距离 $d$&lt;/li&gt;
&lt;li&gt;然后依然从 $V_2$ 中选择点，但此时是选择 $d$ 值最小的点
2.1 如果这个时候选择的点的 $d$ 值是无穷大，说明没有最小生成树
2.2 如果没有 step 2.1，则将当前点放入 $V_1$，然后更新权值 &lt;code&gt;cnt&lt;/code&gt;
2.3 更新集合 $V_1$ 到集合 $V_2$ 里面所有点的最小距离 $d$&lt;/li&gt;
&lt;li&gt;重复 step 2 共 $n - 1$ 次&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
存图：邻接矩阵
    while (m -- ) {
        int x, y, z; cin &amp;gt;&amp;gt; x &amp;gt;&amp;gt; y &amp;gt;&amp;gt; z;
        g[x][y] = g[y][x] = min(g[x][y], z);
    }
*/

const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N], d[N]; // d 数组表示 V1 到 V2 所有点的最小值 
bool st[N]; // st 用来区别 V1:true 和 V2:false 集合

int prim() {
    
    memset(d, 0x3f, sizeof d);
    d[1] = 0; // 这里可以强行选择初始点 1，d[1] = 0 对答案累加也没有影响

    int cnt = 0;
    for (int i = 0; i &amp;lt; n; i ++ ) {
        int t = -1;
        for (int j = 1; j &amp;lt;= n; j ++ )
            if (!st[j] &amp;amp;&amp;amp; (t == -1 || d[j] &amp;lt; d[t])) t = j;
        
        // 如果选择的点没有跟集合 V1 的点直接相连，则说明没有最小生成树
        if (d[t] == INF) return INF;
        
        cnt += d[t];
        st[t] = true;
        
        for (int j = 1; j &amp;lt;= n; j ++ )
            d[j] = min(d[j], g[t][j]);
    }
    return cnt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;kruskal 算法&lt;/h1&gt;
&lt;p&gt;时间复杂度：$O(mlogm)$&lt;/p&gt;
&lt;p&gt;特点：无向图，图中可能存在重边和自环，边权可能为负数。&lt;/p&gt;
&lt;p&gt;图的存储：结构体存边，按照权值可比较&lt;/p&gt;
&lt;p&gt;算法流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对边从小到大进行排序&lt;/li&gt;
&lt;li&gt;然后从前往后遍历每条边
2.1 如果两点在一个集合，则跳过这条边
2.2 否则，加入这条边，然后把两个点的集合存到一起&lt;/li&gt;
&lt;li&gt;最后判断并查集是否只有一个根，是则最后计算的权值是正确的，否则说明无最小生成树&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
const int N = 200010, INF = 0x3f3f3f3f;

struct edge {
    int from, to, w;
    bool operator &amp;lt; (const edge&amp;amp; a) const {
        return w &amp;lt; a.w;
    }
} g[N];
int n, m;
int fa[N];

// 并查集
int get(int x) {
    if (fa[x] != x) fa[x] = get(fa[x]);
    return fa[x];
}

int kruskal() {
    sort(g, g + m); 
    for (int i = 1; i &amp;lt;= n; i ++ ) fa[i] = i;
    
    int res = 0;
    for (int i = 0; i &amp;lt; m; i ++ ) {
        int a = get(g[i].from), b = get(g[i].to);
        if (a == b) continue;
        res += g[i].w;
        fa[a] = b;
    }
    
    int cnt = 0;
    for (int i = 1; i &amp;lt;= n; i ++ ) cnt += fa[i] == i;
    
    return cnt &amp;gt; 1 ? INF : res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;次小生成树&lt;/h1&gt;
&lt;h2&gt;定理&lt;/h2&gt;
&lt;p&gt;对于一个无向图，如果存在最小生成树和（严格）次小生成树，那么对于任意一棵最小生成树，都存在一棵（严格）次小生成树，使得这两棵树只有一条边不同&lt;/p&gt;
&lt;p&gt;那么根据这里定理，假设已经利用 &lt;code&gt;kruskal&lt;/code&gt; 算法求出最小生成树的所有边集，那么严格次小生成树一定是有一条边与最小生成树的一条边不同，因此，我们可以&lt;strong&gt;从未选择的边从小到大枚举，如果将生成树的某条边替换成这条边能得到的权值是最小的&lt;/strong&gt;，那么便是次小生成树。&lt;/p&gt;
&lt;p&gt;在考虑每一条未选择的边的时候，如果把这条边加上，一定是会形成一个环，因此，我们需要考虑从这个环上面寻找一条最大且不等于这条添加的边的权值的边将其删掉，这样得到的树才可能是次小生成树。&lt;/p&gt;
&lt;p&gt;在这里面，问题转化成立寻找一个环里面最大值和次大值，&lt;/p&gt;
&lt;p&gt;求一个环里面的最大值和次大值有两个方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;让任意一个点作根，记录这个点到其任意孩子的最大值和次大值，从而假设未选择的边是 $(a,b)$ , 那么最小生成树一定有一条路径 $(a,b)$ ，因此从 $a$ 为根节点到 $b$ 的最大值和次大值能预处理出来，然后在枚举每一条未选择的边的时候进行计算。&lt;/li&gt;
&lt;li&gt;利用 &lt;code&gt;lca&lt;/code&gt; 进行计算，假设树根已经确定好了，那么 $(a,b)$ 一定有公共祖先，那么公共祖先到 $a$ 或者 $b$ 的最小值和次小值可以预处理，进而，$a$ 到 $b$ 的最小值和次小值可以计算&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;生成树的个数&lt;/h1&gt;
&lt;h2&gt;矩阵树定理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;构建拉普拉斯矩阵 $C$，计算是通过图的度对角矩阵减去图的邻接矩阵&lt;/li&gt;
&lt;li&gt;拉普拉斯矩阵任意一个主子式的行列式值都等于生成树的数量&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;最小生成树的个数&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;定理&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;最小生成树的权值以及相同权值的个数的边的数量是一致的&lt;/li&gt;
&lt;li&gt;在每次处理完相同权值的边，无论这些权值相同的边怎么选择，最终此时图的连通块数量及其状态是一致的（意思是不仅连通块数量一样，而且连通块对应的点也是一样的）
根据定理2，我们在每次处理完一类权值相同的边之后，都可能把这些连通块进行缩点（根据并查集），之后根据乘法原理求最小生成树的数量。注意：此做法要求&lt;strong&gt;相同权值的边的数量很少&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;矩阵树定理&lt;/h2&gt;
&lt;p&gt;如果相同权值的点太多，这题便需要用到矩阵树定理进行优化。根据定理2，当我们处理到某一类权值相同的边权的时候，这个时候的连通块数量是相同的，并且此时同样要处理权值相同的一类边，因此可以将其理解为求生成树的个数，这样就完美利用到了矩阵树定理。最后利用乘法原理计算答案即可。&lt;/p&gt;
</content:encoded></item><item><title>最短路模型</title><link>https://lzleureka.cn/posts/graphtheory/%E6%9C%80%E7%9F%AD%E8%B7%AF%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://lzleureka.cn/posts/graphtheory/%E6%9C%80%E7%9F%AD%E8%B7%AF%E6%A8%A1%E5%9E%8B/</guid><description>各种最短路模型与应用 ~ </description><pubDate>Tue, 26 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;单源最短路&lt;/h1&gt;
&lt;h2&gt;朴素版 dijkstra&lt;/h2&gt;
&lt;p&gt;时间复杂度：$O(n^2)$
限制：图中可能存在重边和自环，所有边权均为正值。
应用：一般应用于点的数量不多，边的数量比较多，也就是&lt;strong&gt;稠密图&lt;/strong&gt;
算法流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化 &lt;code&gt;dist&lt;/code&gt; 数组，源点设置为 0&lt;/li&gt;
&lt;li&gt;在所有的 &lt;code&gt;dist&lt;/code&gt; 数组中找到一个没有更新过的点(&lt;code&gt;!st[j]&lt;/code&gt;)，且 &lt;code&gt;dist&lt;/code&gt; 的值最小的下标 &lt;code&gt;p&lt;/code&gt;，并设置 &lt;code&gt;st[p]=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;遍历 &lt;code&gt;p&lt;/code&gt; 的所有出度边，更新 &lt;code&gt;dist&lt;/code&gt; 数组&lt;/li&gt;
&lt;li&gt;重复 step 2 和 step 3 共 n - 1 次&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
图的预处理
memset(g, 0x3f, sizeof g);
for (int i = 0; i &amp;lt; N; i ++ ) g[i][i] = 0;

读入边时的处理
g[u][v] = min(g[u][v], w);
*/

const int N = 510, INF = 0x3f3f3f3f;

int n, m; 
int g[N][N], dist[N];
bool st[N];

int dijkstra() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    for (int i = 0; i &amp;lt; n; i ++ ) {
        int p = -1;
        for (int j = 1; j &amp;lt;= n; j ++ )
            if (!st[j] &amp;amp;&amp;amp; (p == -1 || dist[p] &amp;gt; dist[j])) p = j;
        st[p] = true;
        
        for (int j = 1; j &amp;lt;= n; j ++ )
            if (dist[p] + g[p][j] &amp;lt; dist[j])
                dist[j] = dist[p] + g[p][j];
    }
    return dist[n] == INF ? -1 : dist[n];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;堆优化的 dijkstra&lt;/h2&gt;
&lt;p&gt;时间复杂度：$O(mlogn)$
限制：图中可能存在重边和自环，所有边权均为正值。
应用：一般应用于&lt;strong&gt;稀疏图&lt;/strong&gt;中
算法流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;同样地，初始化 &lt;code&gt;dist&lt;/code&gt; 数组，源点设置为0，并且以 &lt;code&gt;{dist值，点的下标}&lt;/code&gt; 放入小根堆中&lt;/li&gt;
&lt;li&gt;从小根堆中取出堆顶，判断堆头的点是否已经遍历(&lt;code&gt;st[id]&lt;/code&gt;)，是则继续取出堆顶，否则将该点设置为 &lt;code&gt;st[id]=true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;遍历该点的所有边，如果满足三角不等式 &lt;code&gt;dist[v] &amp;gt; dist[id]+w[i]&lt;/code&gt; 则更新 &lt;code&gt;dist[v]&lt;/code&gt; 的值，然后将其压入堆中&lt;/li&gt;
&lt;li&gt;重复 step 2 和 step 3，直到堆为空&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
图的存储：链式前向星
注意初始化 h 数组为 -1
*/

const int N = 150010, INF = 0x3f3f3f3f;

typedef pair&amp;lt;int, int&amp;gt; PII;

int n, m;
int h[N], e[N], ne[N], w[N], idx = 0;
int dist[N];
bool st[N];

int dijkstra() {
    priority_queue&amp;lt;PII, vector&amp;lt;PII&amp;gt;, greater&amp;lt;PII&amp;gt;&amp;gt; Q;
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    Q.push({0, 1});
    
    while (Q.size()) {
        auto [val, id] = Q.top(); Q.pop();
        if (st[id]) continue;
        st[id] = true;
        
        for (int i = h[id]; ~i; i = ne[i]) {
            int v = e[i];
            if (dist[v] &amp;gt; dist[id] + w[i]) {
                dist[v] = dist[id] + w[i];
                Q.push({dist[v], v});
            }
        }
    }
    return dist[n] == INF ? -1 : dist[n];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;bellman-ford 算法&lt;/h2&gt;
&lt;p&gt;时间复杂度：$O(nm)$
特点：图中可能存在重边和自环， &lt;strong&gt;边权可能为负数&lt;/strong&gt;。图中可能 &lt;strong&gt;存在负权回路&lt;/strong&gt; 。
应用：最多走 $k$ 条边的最短路，判断回路中是负权回路
算法流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化 &lt;code&gt;d&lt;/code&gt; 数组为无穷大，然后源点的值设置为 0&lt;/li&gt;
&lt;li&gt;遍历每条边，满足三角不等式 $d[from] + w &amp;lt; d[v]$ ，做一次松弛操作&lt;/li&gt;
&lt;li&gt;重复 step 2 一共 $n - 1$ 次，如果边权没负数，则此时已求出最短路；如果边权有负数，再重复一遍 step 2，判断 &lt;code&gt;d&lt;/code&gt; 数组有无变化，有则说明存在负权回路
&lt;strong&gt;注意：&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果是指明最多 &lt;code&gt;k&lt;/code&gt; 条边的最短路，则松弛操作只能用上一轮的结果；还有松弛操作是用 &lt;code&gt;d[y]=backup[x] + w&lt;/code&gt; 可能更新多次不会改变遍历多条边？不是，你只能让从上一轮到这一轮的&lt;strong&gt;答案更小&lt;/strong&gt;，并不会多更新边&lt;/li&gt;
&lt;li&gt;另外，如果是判断源点到某汇点是否有最短路，不能直接判断是否等于&lt;code&gt;INF&lt;/code&gt;，而是用是否大于 &lt;code&gt;INF / 2&lt;/code&gt;来判断&lt;/li&gt;
&lt;li&gt;再注意，如果是有负权边，则没有最短路径不能返回 -1，因为 -1 可能是答案
代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;const int N = 10010;
const int INF = 0x3f3f3f3f;

int n, m, k;
struct Node {
    int a, b, w;
} g[N];
int d[N], backup[N];

int bellman_ford() {

    memset(d, 0x3f, sizeof d);
    d[1] = 0;
    for (int i = 0; i &amp;lt; k; i ++ ) {

        memcpy(backup, d, sizeof backup);
        for (int j = 0; j &amp;lt; m; j ++ ) {
            int x = g[j].a, y = g[j].b, w = g[j].w;
            if (d[y] &amp;gt; backup[x] + w)
                d[y] = backup[x] + w;
        }

    }
    if (d[n] &amp;gt; INF / 2) return -1;
    return d[n];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;spfa 算法&lt;/h2&gt;
&lt;p&gt;最坏时间复杂度：$O(nm)$&lt;/p&gt;
&lt;p&gt;特点：图中可能存在重边和自环， &lt;strong&gt;边权可能为负数&lt;/strong&gt;。图中可能 &lt;strong&gt;存在负权回路&lt;/strong&gt; 。
应用：判断回路中是否有负权回路&lt;/p&gt;
&lt;p&gt;spfa 算法本质上是优化了 bellman-ford 算法，因为 bellman-ford 算法是每次暴力枚举所有的边然后无差别松弛，但是事实上只有更新过的点对其进行松弛才有意义。因此，spfa做得优化就是使用队列存储进行过松弛的点，然后利用这些点再进行更新。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;st[i]&lt;/code&gt; 表示点 $i$ 是否在队列里面&lt;/li&gt;
&lt;li&gt;然后使用的是循环队列，因为每个点可能不止入队，出队一次，但是里面最多是有 $n$ 个点，所以循环队列最优&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int N = 100010;

int n, m;
int h[N], e[N], ne[N], w[N], q[N], d[N], idx, hh = 0, tt = 0;
bool st[N]; 

int spfa() {
    
    memset(d, 0x3f, sizeof d);
    d[1] = 0, q[0] = 1, st[1] = true;
    int hh = 0, tt = 1;
    
    while (hh != tt) {
        int t = q[hh ++ ];
        if (hh == N - 1) hh = 0;
        st[t] = false;
        
        for (int i = h[t]; ~i; i = ne[i]) {
            int v = e[i];
            if (d[t] + w[i] &amp;lt; d[v]) {
                d[v] = d[t] + w[i];
                if (!st[v]) {
                    q[tt ++ ] = v;
                    if (tt == N - 1) tt = 0;
                    st[v] = true;
                }
            }
        }
    }
    return d[n];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;spfa 判断负环&lt;/h3&gt;
&lt;p&gt;判断负环重要的环节就是判断一个点被松弛的次数是否 $\ge n$，或者最短路里的边数 $\ge n$  ，因此在原来spfa算法的基础上，加入 &lt;code&gt;cnt&lt;/code&gt; 数组来记录即可&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果只是判断负环，那么由于图可能不连通，所以初始化需要把所有点都加入到队列里面，然后每个点的 &lt;code&gt;d&lt;/code&gt; 值都为 $0$，这个就好比加入了一个超级源点，然后超级源点连接了图中每一个点，且权值为 $0$&lt;/li&gt;
&lt;li&gt;注意，&lt;code&gt;d&lt;/code&gt; 数组在松弛的过程中不可不更新&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int n, m; 
int h[N], e[N], ne[N], w[N], idx = 0;
int d[N], cnt[N], q[N], hh = 0, tt = 0;
bool st[N];

bool spfa() {
    
    for (int i = 1; i &amp;lt;= n; i ++ ) {
        st[i] = true;
        q[tt ++ ] = i;
    }
    
    while (hh != tt) {
        int u = q[hh ++ ];
        if (hh == N - 1) hh = 0;
        
        st[u] = false;
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (d[u] + w[i] &amp;lt; d[v]) {
                d[v] = d[u] + w[i];
                cnt[v] = cnt[u] + 1;
                if (cnt[v] &amp;gt;= n) return true;
                if (!st[v]) {
                    st[v] = true;
                    q[tt ++ ] = v;
                    if (tt == N - 1) tt = 0;
                }
            }
        }
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;spfa 找正环例题&lt;/h3&gt;
&lt;p&gt;spfa 找正环的广度比我想象的还要广：比如 &lt;a href=&quot;https://www.acwing.com/problem/content/description/4245/&quot;&gt;货币兑换&lt;/a&gt; ，点的权值变化是 $x_j = (x_i - C_i )* R_i$ ，判断是否有正环。这得益于 spfa 只要松弛了就需要继续更新其他点。所以，彻底理解spfa的机制还是非常有价值的&lt;/p&gt;
&lt;h3&gt;0/1 分数规划&lt;/h3&gt;
&lt;p&gt;形如 $\frac{\sum{f_i}}{\sum{w_i}}$ 的式子的最大值或者最小值，可以使用 [[二分&amp;amp;三分#二分]] + [[#spfa 判断负环]] 来求解&lt;/p&gt;
&lt;p&gt;对于这样的问题，我们统一使用 0/1 规划求解，首先二分答案为 $mid$，然后假设&lt;strong&gt;存在一个环&lt;/strong&gt; $\frac{\sum{f_i}}{\sum{w_i}} \gt mid$ ，那么其答案一定在 $[mid,r]$ 之间；如果对于&lt;strong&gt;任意的环&lt;/strong&gt;，都使得 $\frac{\sum{f_i}}{\sum{w_i}} \lt mid$ ，那么答案肯定在 $[l,mid]$ 之间。&lt;/p&gt;
&lt;p&gt;首先对于 $\frac{\sum{f_i}}{\sum{w_i}} \gt mid$，我们做一些转化
$$
\frac{\sum{f_i}}{\sum{w_i}} \gt mid \iff \sum (f_i - mid * w[i]) \gt 0
$$
换句话说，判断一个环&lt;strong&gt;是否存在&lt;/strong&gt;有 $\frac{\sum{f_i}}{\sum{w_i}} \gt mid$ 等价于判断对边权进行修改后的图&lt;strong&gt;是否存在有正环&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;注意，为什么不用 $\frac{\sum{f_i}}{\sum{w_i}} \lt mid$ 做判断，其原因就在于我们要对于 &lt;strong&gt;任意的环&lt;/strong&gt;，而我们没有判断任意环都是负环的做法。&lt;/p&gt;
&lt;p&gt;因此，对于 $0/1$ 规划的问题，&lt;strong&gt;一定要从存在的前缀的不等式那边找环&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;多源最短路&lt;/h1&gt;
&lt;h2&gt;Floyd 算法&lt;/h2&gt;
&lt;p&gt;时间复杂度：$O(n^3)$&lt;/p&gt;
&lt;p&gt;特点：可以同时计算任意两点之间的最短距离。图中可能存在重边和自环，边权可能为负数。&lt;/p&gt;
&lt;p&gt;floyd 算法的思路基于动态规划：$g(k,i,j)$ 表示只通过前 $k$ 个点进行松弛，点 $i$ 到点 $j$ 的最短路径。&lt;/p&gt;
&lt;p&gt;转移：对于 $g(k,i,j)$ 来说，值从两种情况得到，一种是 $i$ 到 $j$ 的最短路径没有包含 $k$，一种是包含了 $k$。对于第一种情况，就是 $g(k-1,i,j)$，对于第二中情况，我们知道 $k$ 一定是 $i$ 到 $j$ 路径上的某个点，所以此时将路径 $(i,j)$ 分成 $(i,k),(k,j)$ 两部分，另外在这两部分里面都是没有包含 $k$ 的，最多是 $k-1$。
因此，$g(k,i,j)=min(g(k-1,i,j),g(k-1,i,k)+g(k-1,k,j))$。&lt;/p&gt;
&lt;p&gt;滚动数组：可以看到 $g(k,i,j)$ 的转移之和上一轮的结果有关，因此去掉一维就是 $g(i,j)=min(g(i,j),g(i,k)+g(k,j))$ ，由于 $g(k,i,k)=g(k-1,i,k),g(k,k,j)=g(k-1,k,j)$  直接去掉一维是等价的&lt;/p&gt;
&lt;p&gt;初始化：最开始的 $g$ 值表示没有松弛的时候，也就是原图初始的样子。&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int g[N][N];

void Floyd() {
    
    for (int k = 1; k &amp;lt;= n; k ++ )
        for (int i = 1; i &amp;lt;= n; i ++ )
            for (int j = 1; j &amp;lt;= n; j ++ )
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}

//初始化
while (m -- ) {
    int x, y, z; cin &amp;gt;&amp;gt; x &amp;gt;&amp;gt; y &amp;gt;&amp;gt; z;
    g[x][y] = min(g[x][y], z);
}

// 判断结果
if (g[x][y] &amp;gt; 0x3f3f3f3f / 2) puts(&quot;impossible&quot;);
else cout &amp;lt;&amp;lt; g[x][y] &amp;lt;&amp;lt; endl;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;传递闭包&lt;/h3&gt;
&lt;p&gt;传递闭包说到底就是值任意两个点的可达性，比如说 A 能到 B，B 能到 C，那么 A 就能到 C。可以使用 Floyd 求传递闭包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (int k = 1; k &amp;lt;= n; k ++ )
	for (int i = 1; i &amp;lt;= n; i ++ )
		for (int j = 1; j &amp;lt;= n; j ++ )
			g[i][j] |= g[i][k] &amp;amp;&amp;amp; g[k][j];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;拓展&lt;/strong&gt;
假设 &lt;code&gt;g[i][j]&lt;/code&gt; 表示 $i &amp;gt; j$ 的情形，如果根据传递闭包判断每个人的排名是否已经确定？？
答：如果一个人的排名已经确定，小于这个人的人数+大于这个人的人数一定等于 n - 1&lt;/p&gt;
&lt;h3&gt;最小环问题&lt;/h3&gt;
&lt;p&gt;给定一张无向图，求图中一个至少包含 $3$ 个点的环，环上的节点不重复，并且环上的边的长度之和最小。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解答&lt;/strong&gt;
我们知道，所有至少有 $3$ 个点的环，一定有一个编号最大的点 $k$ ，然后我们可以枚举这个点附近的两个点 $i,j（i&amp;lt;k, j&amp;lt;k)$ ，那么 $(i,k)$ 和 $(k,j)$ 的长度我们是已知的，那么问题就变成要求 $(i,j)$ 之间的最短路且不经过 $\ge k$ 的点。&lt;/p&gt;
&lt;p&gt;那么这就能联想 &lt;code&gt;floyd&lt;/code&gt; 的动态规划表 $f(k,i,j)$ 求的是只考虑前 $k$ 个点 $(i,j)$ 的最短路，因此，求解最小环的长度便可以在求 &lt;code&gt;floyd&lt;/code&gt; 的过程中求解出来。&lt;/p&gt;
&lt;p&gt;那么如果是求解一条最小环呢，也就是一个最小环的方案，这个同样也可以在求的过程中，一旦求出当前最小的环，然后把方案求解出来。因此，我们只需要求解 $(i,j)$ 的最短路的方案即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在求 $(i,j)$ 的最短路方案的时候，不需要还原滚动数组&lt;/strong&gt;，因为最短路还是满足一个三角不等式的 $d(i,j) \le d(i,k)+ d(k,j)$ 的，不妨假设 $pre(i,j)=k$，表示 $d(i,j)$ 是从中间点 $k$ 转移的，那么一定是有 $d(i,k)$ 和 $d(k,j)$ 的路径不重复的。
因此，如果在当前有 $d(i,j)=d(i,k)+d(k,j)$，那么 $(i, ... ,k, ..., j)$ 是可行方案。&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
数据结构和求路径函数
注意这里不能使用 f[i][j]==g[i][j] 来判断(i,j)中间是否有其他的点
int g[N][N], f[N][N], pre[N][N];
int path[N], cnt = 0;

void get_path(int i, int j) {
    if (!pre[i][j]) return ;
    
    get_path(i, pre[i][j]);
    path[cnt ++ ] = pre[i][j];
    get_path(pre[i][j], j);
}
*/

int res = INF;
for (int k = 1; k &amp;lt;= n; k ++ ) {
    
    if (k &amp;gt; 2) {
        for (int i = 1; i &amp;lt; k; i ++ )
            for (int j = i + 1; j &amp;lt; k; j ++ )
                if (1ll * f[i][j] + g[i][k] + g[k][j] &amp;lt; res) {
                    res = f[i][j] + g[i][k] + g[k][j];
                    cnt = 0;
                    path[cnt ++ ] = k; path[cnt ++ ] = i;
                    get_path(i, j);
                    path[cnt ++ ] = j;
                }
                
    }
    
    for (int i = 1; i &amp;lt;= n; i ++ )
        for (int j = 1; j &amp;lt;= n; j ++ )
            if (f[i][k] + f[k][j] &amp;lt; f[i][j]) {
                pre[i][j] = k;
                f[i][j] = f[i][k] + f[k][j];
            }
    
}
if (res != INF) {
    for (int i = 0; i &amp;lt; cnt; i ++ ) cout &amp;lt;&amp;lt; path[i] &amp;lt;&amp;lt; &apos; &apos;;
    cout &amp;lt;&amp;lt; endl;
} else puts(&quot;No solution.&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;两点之间恰好经过 $k$ 条边的最短路&lt;/h3&gt;
&lt;p&gt;这个稍微和 &lt;code&gt;Floyd&lt;/code&gt; 有点不一样，因为状态的设置不一样。$f(k, i, j)$ 表示从 $i$ 走到 $j$ 恰好经过 $k$ 条边的最短路，假设我们已经计算好了 $f(a,i,j)$ 和 $f(b,i,j)$ ，怎么计算 $f(a+b, i,j)$ ?&lt;/p&gt;
&lt;p&gt;那么根据集合进行划分，$i$ 走了 $a$ 条边到达的是 $k$，因此 $f(a+b, i,j) = min_{k\in [1,n]}{f(a,i,k)+f(b,k,j)}$&lt;/p&gt;
&lt;p&gt;由于这个问题是可以重复经过一些点，因此 $f(a,i,k)$ 和 $f(b,k,j)$ 之间路径互不影响，而且根据公式来看这个式子具有结合律。因此，可以尝试使用矩阵快速幂来加速计算。&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;matrix mul(matrix a, matrix b) {
    matrix c;
    memset(c.m, 0x3f, sizeof c.m);
    for (int k = 1; k &amp;lt;= n; k ++ )
        for (int i = 1; i &amp;lt;= n; i ++ )
            for (int j = 1; j &amp;lt;= n; j ++ )
                c.m[i][j] = min(1ll * c.m[i][j], 1ll * a.m[i][k] + b.m[k][j]);
    return c;
}
// 这里初始的res是任意两个点都不能到达，除了自己到自己最短路是 0
matrix kpow(matrix a, int k) {
    matrix res;
    memset(res.m, 0x3f, sizeof res.m);
    for (int i = 1; i &amp;lt;= n; i ++ ) res.m[i][i] = 0;
    while (k) {
        if (k &amp;amp; 1) res = mul(res, a);
        a = mul(a, a);
        k &amp;gt;&amp;gt;= 1;
    }
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;单源最短路应用&lt;/h1&gt;
&lt;h2&gt;乘积最大值&lt;/h2&gt;
&lt;p&gt;假如有一个图，边权 $0 &amp;lt;w \le 1$，然后求点 $A$ 到 $B$ 的权值最大乘积。
为什么这个可以直接用变形的 dijkstra 求解？
因为 $log(w_1\cdots w_k) = log(w_1)+\cdots+log(w_k)$ ，再加上 $w$ 的范围如此，所以 $log(w_i) \le 0$ ，这个问题就转化成了求 $\sum_{i=1}^klog(w_i)$ 的最大值，也就是 $-\sum_{i=1}^k-log(w_i)$ 的最大值，也就是 $\sum_{i=1}^k-log(w_i)$ 的最小值。由于 $-log(w_i) \ge 0$ ，这就等价于 dijkstra 算法流程。&lt;/p&gt;
&lt;p&gt;如果是有 $w&amp;gt;1$ 情况咋办，可能就需要 spfa 来进行求解。&lt;/p&gt;
&lt;p&gt;如果是有 $w \le 0$ 的情况，这个就暂时不清楚了&lt;/p&gt;
&lt;h2&gt;超级源点（虚拟点）&lt;/h2&gt;
&lt;p&gt;有些题可能需要超级源点连接所有的点（比如差分约束问题），有些题可能需要一个超级源点连接部分点（比如：多源最短路径问题）&lt;/p&gt;
&lt;h2&gt;求经过某个点的最小正环&lt;/h2&gt;
&lt;p&gt;首先从这个点出发，待到这个点更新了其他点，将这个点的最小距离更新为无穷大，因为一开始本质上是求从这个点到其他点的最短路，因此该操作也会求得经过这个点的最小正环&lt;/p&gt;
&lt;h2&gt;建反图&lt;/h2&gt;
&lt;p&gt;如果要求任意点到某个点的最短路，建立反图是不错的选择&lt;/p&gt;
&lt;h1&gt;差分约束&lt;/h1&gt;
&lt;p&gt;差分约束的作用有 &lt;strong&gt;二&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求某个特殊不等式组的一组可行解&lt;/li&gt;
&lt;li&gt;求所有点最大值或最小值&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;求某个特殊不等式组的可行解&lt;/h2&gt;
&lt;p&gt;特殊不等式组：诸如 $x_i \le x_j + C_{ij}$ 或者 $x_i \ge x_j + C_{ij}$ 的不等式组
我们可以将这样的不等式组转化成一个单源最短路问题，以 $x_i \le x_j + C_{ij}$ 为例：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对每个不等式 $x_i \le x_j + C_{ij}$，转化成一条 $x_j$ 到 $x_i$ 的边，权值为 $C_{ij}$ 的一条边&lt;/li&gt;
&lt;li&gt;找一个超级源点，使得该源点一定可以遍历到所有边&lt;/li&gt;
&lt;li&gt;从源点求一遍单源最短路
&lt;ol&gt;
&lt;li&gt;结果1：如果存在负环，则不等式组无解&lt;/li&gt;
&lt;li&gt;结果2：如果没有负环，则从源点到每个点的最短路径就是一组可行解&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确性分析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于第一点，对于不等式 $x_i \le x_j + C_{ij}$，转化成边之后，根据最短路的结果一定有 $dist[x_i] \le dist[x_j] + C_{ij}$ ，因为如果有 $dist[x_i] \gt dist[x_j] + C_{ij}$ ，则说明 $dist[x_i]$ 还能再更新，这与最短路的结果相矛盾&lt;/li&gt;
&lt;li&gt;至于为什么要找超级源点？因为如果只是随便从某一个点出发，由于这个图可能不连通，或者到不了某些点，导致在图的遍历过程中并未有访问所有边，因为从第一点来说，我们是要&lt;strong&gt;访问所有边&lt;/strong&gt;，而不是所有点，因此建立一个超级源点十分有必要&lt;/li&gt;
&lt;li&gt;如果存在负环，为什么不等式无解？可以从建立边的流程出发，首先在图中有环，说明在不等式也有个类似的“环”，不妨假设
$$
x_1 \le x_2 + C_1 , x_2 \le x_3 + C_2, \cdots , x_k \le x_1 + C_k
$$
这在图中等价一个环，然后假设这个环是负环，也就是 $C_1+\cdots+C_k &amp;lt; 0$ ，将这个不等式合并会发生什么？
$$
x_1 \le x_2 + C_1 \le x_3 + C_1 + C_2 \le \cdots \le x_1 + C_1 + \cdots C_k
$$
然后再消掉 $x_1$，发现 $C_1+\cdots+C_k \ge 0$ ，这与之前的假设矛盾，因此无解。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;拓展&lt;/strong&gt;
刚刚讨论了 $x_i \le x_j + C_{ij}$ 不等式组的可行解求法，那么如果是 $x_i \ge x_j + C_{ij}$ 该如何处理呢？事实上，前两步是一样的，也是建图加找超级源点，但是后面就不是寻找最短路了，而是&lt;strong&gt;寻找最长路，然后判断有没有解也是判断有无正环&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;求所有点最大值或最小值&lt;/h2&gt;
&lt;p&gt;事实上，要想求最大值或者最小值，还需要有 $x_i \le C_i$ 或者 $x_i \ge C_i$ 的约束。
同样地，这也是可以转化成最短路和最长路模型。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;建图（与上述可行解建图相同）&lt;/li&gt;
&lt;li&gt;超级源点满足 $x_i\le C_i$ 或 $x_i \ge C_i$ 的建图，也就是 $x_i \le x_0 + C_i$&lt;/li&gt;
&lt;li&gt;结论：如果求的是最大值（$\le$)，应该求最短路；如果是求最小值（$\ge$），应该求最长路&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确性分析：
以求 $x_i$ 的最大值为例子：从任意一个点 $x_i$ 出发，都可以构成任意多个不等式链 $x_i \le x_j + C_1 \le x_k + C_1 + C_2 \cdots \le C_1 + C_2 + \cdots$ ，每从图上走过一条路径都是等价于这样的一个不等式。因此这样每个点都有很多这样的上界，最终 $x_i$ 的最大值就是这些上界的最小值。&lt;/p&gt;
&lt;h1&gt;最短路数量与次短路数量问题&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;由于根据dijkstra算法的更新特性，最后更新完毕的路线一定是一个拓扑图&lt;/strong&gt;，因此我们可以从 &lt;code&gt;dp&lt;/code&gt; 的角度出发，$dist[i]$ 表示 $1-i$ 的最小路径，$cnt[i]$ 表示 $1-i$ 中最短路径的条数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../images/%E6%9C%80%E7%9F%AD%E8%B7%AF%E6%95%B0%E9%87%8F.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同样地，次短路最后更新完毕的路线也是这样的拓扑图。我们先看次短路怎么来的，首先肯定也是和最短路类似，我们可以枚举它可能的上一个结点，那么它可能从上一个结点的最短路转移过来，只要转移过来的值不等于当前点的最短路即可，当然也可能从一个结点的次短路转移过来，如果是从次短路转移过来，那么这个肯定就不是等于当前点的最短路。&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void dijkstra() {
    for (int i = 1; i &amp;lt;= n; i ++ ) 
        for (int j = 0; j &amp;lt; 2; j ++ )
            d[i][j] = INF, st[i][j] = false, f[i][j] = 0;
            
    d[S][0] = 0, f[S][0] = 1;
    priority_queue&amp;lt;Node, vector&amp;lt;Node&amp;gt;, greater&amp;lt;Node&amp;gt;&amp;gt; Q;
    Q.push({0, S, 0});
    
    while (Q.size()) {
        auto [val, u, type] = Q.top(); Q.pop();
        if (st[u][type]) continue;
        st[u][type] = true;
        
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (d[v][0] &amp;gt; d[u][type] + w[i]) {
                d[v][1] = d[v][0]; f[v][1] = f[v][0]; Q.push({d[v][1], v, 1});
                d[v][0] = d[u][type] + w[i];
                f[v][0] = f[u][type];
                Q.push({d[v][0], v, 0});
            } else if (d[v][0] == d[u][type] + w[i]) f[v][0] += f[u][type];
            else if (d[v][1] &amp;gt; d[u][type] + w[i]) {
                d[v][1] = d[u][type] + w[i];
                f[v][1] = f[u][type];
            } else if (d[v][1] == d[u][type] + w[i]) f[v][1] += f[u][type];
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>数论和线性代数</title><link>https://lzleureka.cn/posts/math/math-maththeory/</link><guid isPermaLink="true">https://lzleureka.cn/posts/math/math-maththeory/</guid><description>关于数论知识和线性代数知识与代码模板总结</description><pubDate>Mon, 23 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;数论&lt;/h1&gt;
&lt;h2&gt;快速幂&lt;/h2&gt;
&lt;h3&gt;快速幂&lt;/h3&gt;
&lt;p&gt;快速幂主要是能在 $O(log k)$ 的时间复杂度求解 $a^k % m$ 。其思路在于任何一个数都可以写成以下形式 $k=2^{c_1} + 2^{c_2} + \cdots + 2^{c_y}$ 。{$c_i$} 序列是严格单调递增的。因此 $a^k \ % \  m$ 可以写成 $a^{2^{c_1} + 2^{c_2} + \cdots + 2^{c_y}} \ % \ m$ ，从而一步一步往下计算 $a^{2^{c_i}}$ ，最多计算 $logk$ 次即可。&lt;/p&gt;
&lt;p&gt;核心代码如下 ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int kpow(int a, int k, int m) {
    
    a %= m;
    int ans = 1 % m;
    while (k) {
        if (k &amp;amp; 1) ans = (LL) ans * a % m;
        k &amp;gt;&amp;gt;= 1;
        a = (LL) a * a % m;
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;龟速乘&lt;/h3&gt;
&lt;p&gt;龟速乘主要是 $O(logb)$ 的时间内计算 $a&lt;em&gt;b$，这种计算方法主要应用在 $a&lt;/em&gt;b$会爆掉 &lt;code&gt;long long&lt;/code&gt; 的情况下，思路和快速幂一致，也是利用 $b$ 的二进制拆分。&lt;/p&gt;
&lt;p&gt;核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LL kmul(LL x, LL y, LL m) {
    LL ans = 0;
    while (y) {
        if (y &amp;amp; 1) ans = (ans + x) % m;
        x = (x + x) % m;
        y &amp;gt;&amp;gt;= 1;
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;素数筛&lt;/h2&gt;
&lt;p&gt;首先来看 1-N 中有多少个素数，也就是 $\pi(N) \approx \frac{N}{lnN}$ 。当 $N=1e6$ 时，$\pi(N)=78498$ ; 当 $N=1e7$ 时，$\pi(N)=664579$。了解在什么范围内有多少素数，有利于掌握相应的时间复杂度。&lt;/p&gt;
&lt;p&gt;下面给出三种筛法（杜教筛以后学到再说）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;埃式筛
时间复杂度为 $O(nloglogn)$ 。
核心代码如下：&lt;pre&gt;&lt;code&gt;int primes[N], cnt = 0;
bool st[N];

void get_primes(int n) {
    cnt = 0;
    memset(st, false, sizeof st);
    
    for (int i = 2; i &amp;lt;= n; i ++ ) {
        if (st[i]) continue;
        primes[cnt ++ ] = i;
        for (int j = i + i; j &amp;lt;= n; j += i)
            st[j] = true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;线性筛
时间复杂度为 $O(n)$。
核心思路在于利用每个数的最小质数将其筛掉
核心代码如下：&lt;pre&gt;&lt;code&gt;int primes[N], cnt = 0;
bool st[N];

void get_primes(int n) {
    cnt = 0;
    memset(st, 0, sizeof st);
    
    for (int i = 2; i &amp;lt;= n; i ++ ) {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; i &amp;lt;= n / primes[j]; j ++ ) {
            st[i * primes[j]] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;杜教筛
时间复杂度为 $O(n^\frac{2}{3})$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;素数判定&lt;/h2&gt;
&lt;h3&gt;试除法&lt;/h3&gt;
&lt;p&gt;时间复杂度：$\mathcal{O}(\sqrt{n})$
当 $n \le 10^{12}$ 时，足够判断
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool is_prime(int x) {
    
    if (x == 1) return false;
    for (int i = 2; i &amp;lt;= x / i; i ++ )
        if (x % i == 0) return false;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，这个方法还可以加快速度，根据[[#唯一分解定理]]，只需要使用 $[2,\sqrt n]$ 里面的素数来判断即可。 因此，这里有个策略是利用线性筛提前预处理好前 $\sqrt n$ 的所有素数，但由于时间和空间限制，这个 $n$ 的范围最大是 $n \le 10^{14}$
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool is_prime(int x) {
    
    if (x == 1) return false;
    for (int i = 0; prime[i] &amp;lt;= x / prime[i]; i ++ )
        if (x % prime[i] == 0) return false;
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Miller-Rabin 算法&lt;/h3&gt;
&lt;h4&gt;费马小定理&lt;/h4&gt;
&lt;p&gt;设 $n$ 是素数，$a$ 是正整数且与 $n$ 互素，那么有 $a^{n-1} \equiv 1(\text{mod}\ n)$&lt;/p&gt;
&lt;p&gt;费马小定理目前作用有二：&lt;strong&gt;1. 这个是判断素数的必要条件；2.同余求逆&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于应用一：要判断 $n$ 是否为素数，可以随机 $a$ 来判断 $a^{n-1}$ 模 $n$ 是否为 1，如果不为 1 ，一定不是素数，如果为 $1$ ， 可能是素数。因为一定有一些数（Carmichael 数）满足该式子，但是不为素数。但是这种数出现的概率不大。&lt;/p&gt;
&lt;p&gt;对于应用二：由于 $a * a^{n-2} \equiv 1(\text{mod} \  n)$ ，&lt;strong&gt;因此 $a^{n-2}$ 是在 $a$ 与 $n$ 互素下同余 $n$ 的关于 $a$ 的逆元&lt;/strong&gt;。注意这个后缀很重要，如果不互素，得使用扩展欧几里得&lt;/p&gt;
&lt;h4&gt;二次探测定理&lt;/h4&gt;
&lt;p&gt;如果 $p$ 是&lt;strong&gt;一个奇素数&lt;/strong&gt;，且 $e \ge 1$，则方程 $x^2 \equiv 1 (\text{mod} \ p^e)$ 仅有两个解：$x=1$ 和 $x=-1$ 。当 $e=1$ 时，方程 $x^2 \equiv 1(\text{mod} \ p)$  仅有两个解 $x=1$ 和 $x=p-1$
把 $x=1$ 和 $x=p-1$ 称为“$x$ 对模 $p$ 来说 $1$ 的平凡平方根”&lt;/p&gt;
&lt;p&gt;我们利用其逆否命题：对于方程 $x^2 \equiv 1(\text{mod} \ n)$ ，（注意 $n$ 不一定是奇素数），如果对模 $n$ 存在 $1$ 的非平凡平方根，&lt;strong&gt;则 $n$ 不可能是奇素数或者奇素数的幂&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据上面两个定理，可设计 Miller-Rabin 算法&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 $n == 1$ ，不是素数&lt;/li&gt;
&lt;li&gt;如果 $n==2$ ，是素数&lt;/li&gt;
&lt;li&gt;如果 $n%2 == 0$，不是素数&lt;/li&gt;
&lt;li&gt;那么剩下的就是 $n\gt 2$ ，且 $n$ 是奇数，来判断其是否为素数
&lt;ol&gt;
&lt;li&gt;首先随机 $a$ ，判断 $a^{n-1}$ 模$n$ 是否为 1，不是则说明不是素数&lt;/li&gt;
&lt;li&gt;等于 1 的话利用二次探测定理继续判断，因此此时 $n-1$ 是偶数，所以有方程 ${a^{\frac{n-1}{2}\cdot 2} } \equiv 1(\text{mod} \ n)$  ，所以可以判断 $a^{\frac{n-1}{2}}$  是否为 $1$ 或者 $n-1$ ，不是则说明不为素数，是则继续判断直到 $\frac{n-1}{2^t}$ 为奇数&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码中对 4.2 的利用不太一样，是直接对 $n -1$ 除到一个奇数，然后从这个奇数开始算到 $n-1$，如果都满足二次探测定理，最后则判断其是否满足费马小定理&lt;/p&gt;
&lt;p&gt;只考虑 &lt;code&gt;int&lt;/code&gt; 时间复杂度：$O(klog^3 n)$  ，出错的概率为 $2^{-s}$
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;random&amp;gt;
random_device rd;
mt19937 gen(rd());

int kpow(int a, int k, int m) {
    
    a %= m;
    int ans = 1;
    while (k) {
        if (k &amp;amp; 1) ans = (LL) ans * a % m;
        k &amp;gt;&amp;gt;= 1;
        a = (LL) a * a % m;
    }
    return ans;
}

bool witness(int a, int n) {
    int u = n - 1, t = 0;
    while ((u &amp;amp; 1) == 0) u &amp;gt;&amp;gt;= 1, t ++ ;
    int x1, x2;
    x1 = kpow(a, u, n);
    for (int i = 0; i &amp;lt; t; i ++ ) {
        x2 = kpow(x1, 2, n);
        if (x2 == 1 &amp;amp;&amp;amp; x1 != 1 &amp;amp;&amp;amp; x1 != n - 1) return true;
        x1 = x2;
    }
    if (x1 != 1) return true;
    return false;
}

bool miller_rabin(int n, int s) {
    
    if (n &amp;lt; 2) return false;
    if (n == 2) return true;
    if (n % 2 == 0) return false;
    // i &amp;lt; min(s, n)，因为随机数取的是 [1,n-1]，当 s &amp;gt; n 时没啥意义
    for (int i = 0; i &amp;lt; s &amp;amp;&amp;amp; i &amp;lt; n; i ++ ) {
        int a = gen() % (n - 1) + 1;
        if (witness(a, n)) return false;
    }
    return true;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;考虑到 &lt;code&gt;long long&lt;/code&gt; 的乘法会溢出的情况下需要使用龟速乘，时间复杂度为：$O(klog^4 n)$ 。&lt;/p&gt;
&lt;h2&gt;分解质因数&lt;/h2&gt;
&lt;h3&gt;唯一分解定理&lt;/h3&gt;
&lt;p&gt;任何 $n\ge 2$ 的整数都可以分解成若干个质数乘积。也就是说
$$
n = p_1^{x_1}p_2^{x_2}p_3^{x_3}\dots p_k^{x_k}
$$&lt;/p&gt;
&lt;h3&gt;质因数分解&lt;/h3&gt;
&lt;p&gt;思路：首先利用一个数 $n$ 的质因数中大于 $\sqrt n$ 的最多只有一个的特性，只需要枚举到 $\sqrt n$ 的情形就行，枚举的过程中，$n$ 可以通过枚举到的质因数逐渐变小，最后退出循环后，只要 $n$ 仍旧大于 1，说明这是剩下来的那个质因数。
时间复杂度：$\mathcal{O}(\sqrt{n})$
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void divide(int x) {
    for (int i = 2; i &amp;lt;= x / i; i ++ ) {
        int cnt = 0;
        while (x % i == 0) cnt ++ , x /= i;
        if (cnt) cout &amp;lt;&amp;lt; i &amp;lt;&amp;lt; &apos; &apos; &amp;lt;&amp;lt; cnt &amp;lt;&amp;lt; endl;
    }
    if (x &amp;gt; 1) cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; &apos; &apos; &amp;lt;&amp;lt; 1 &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，当 $n$ 的范围不是很大的时候，也就是 $10^7$ 左右的时候，可以通过&lt;strong&gt;只枚举素数&lt;/strong&gt;来加快速度。时间复杂度大概是 $\mathcal{O}(\sqrt{\frac{n}{ln n}})$&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void divide(int n) {
    
    for (int i = 0; primes[i] &amp;lt;= n / primes[i]; i ++ ) {
        int cnt = 0;
        while (n % primes[i] == 0) cnt ++ , n /= primes[i];
        if (cnt &amp;gt; 0) {
            cout &amp;lt;&amp;lt; primes[i] &amp;lt;&amp;lt; &apos; &apos; &amp;lt;&amp;lt; cnt &amp;lt;&amp;lt; endl;
        }
    }
    if (n &amp;gt; 1) cout &amp;lt;&amp;lt; n &amp;lt;&amp;lt; &apos; &apos; &amp;lt;&amp;lt; 1 &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，如果要存的话，使用差不多 100 大小的数组就能存，&lt;strong&gt;因为对于 &lt;code&gt;int&lt;/code&gt; 型的数质因数最多有十几个，一个因数的个数最多有三十几个&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;pollard_rho 启发式方法分解质因数&lt;/h4&gt;
&lt;h2&gt;阶乘相关&lt;/h2&gt;
&lt;h3&gt;勒让德定理&lt;/h3&gt;
&lt;p&gt;在正数 $n!$ 的质因数分解中，质数 $p$ 的指数记作 $v_p(n!)$ ，则$$v_p(n!)=\sum_{k\ge 1}\lfloor \frac{n}{p_k} \rfloor$$&lt;/p&gt;
&lt;h3&gt;阶乘质因数分解&lt;/h3&gt;
&lt;p&gt;思路就是唯一分解定理和勒让德定理的应用。
时间复杂度在 $O(n)$ 左右。
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 首先是找质数，根据上面的线性筛

// 然后就是勒让德计算
for (int i = 0; i &amp;lt; cnt; i ++ ) {
        int cnt = 0;
        for (int k = n / primes[i]; k &amp;gt; 0; k /= primes[i]) cnt += k;
        cout &amp;lt;&amp;lt; primes[i] &amp;lt;&amp;lt; &apos; &apos; &amp;lt;&amp;lt; cnt &amp;lt;&amp;lt; endl;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;威尔逊定理&lt;/h3&gt;
&lt;p&gt;若 $p$ 为素数，则 $p$ 可以整除 $(p-1)!+1$
威尔逊定理四种表示方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$((p-1)!+1)\text{mod} \ p = 0$&lt;/li&gt;
&lt;li&gt;$(p-1)!\text{mod} \ p = p-1$&lt;/li&gt;
&lt;li&gt;$(p-1)!=pq - 1$ ，$q$ 为正整数&lt;/li&gt;
&lt;li&gt;$(p-1)!\equiv -1(\text{mod} \ p)$
核心记住第一点&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;约数&lt;/h2&gt;
&lt;h3&gt;约数相关性质&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;约数个数
根据唯一分解定理，约数个数 $f(n)=(x_1+1)&lt;em&gt;(x_2+2)&lt;/em&gt;\cdots*(x_k+1)$
那么前 $n$ 个数的约数个数之和怎么算呢，可以根据第 $i$ 个数在 $1-n$ 中是多少个数的约数，也就是 $\frac{n}{i}$  个，因此 $\sum f(i) = nln n$ 大概是这个量级。这对分析时间复杂度有帮助。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;约数之和
一个数所有约数之和为 $\text{sum}=(1+p_1^1 + \cdots + p_1^{x_1}) * (1+p_2^1 + \cdots + p_2^{x_1}) * \cdots * (1+p_k^1 + \cdots + p_k^{x_k})$ ，这个很容易证明，根据乘法分配法，最后加法分隔开的每个数，都是从每个括号里面选择一个数进行相乘，这样刚好就枚举了所有的约数可能。
代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 第一步分解质因数
int ans = 1;
for (auto [u, v]: um) {
    int p = 1, t = 0;
    for (int i = 0; i &amp;lt;= v; i ++ ) t = (t + p) % MOD, p = 1ll * p * u % MOD;
    ans = 1ll * ans * t % MOD;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最多的约数个数
在 &lt;code&gt;int&lt;/code&gt; 范围内，约数个数最多的数大概有 &lt;strong&gt;1600&lt;/strong&gt; 个约数。这对分析时间复杂度相当有帮助。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;快速求出所有约数
首先第一步，对数先进行质因数分解，然后 dfs 求得所有约数，根据上述的最多约数个数为 1600 个左右，因此 dfs 的时间一般来说是小于质因数分解的速度的。因此，求所有约数的时间极限变成了&lt;strong&gt;如何快速求质因数分解。&lt;/strong&gt;[[#质因数分解]]
其中，dfs 代码为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void dfs(int cur, int val) {
if (cur &amp;gt;= idx) {
    res.push_back(val);
    return ;
}
for (int i = 0, t = 1; i &amp;lt;= c[cur]; i ++ , t *= p[cur])
    dfs(cur + 1, val * t);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;}
```&lt;/p&gt;
&lt;h4&gt;最大公约数&lt;/h4&gt;
&lt;h5&gt;欧几里得算法&lt;/h5&gt;
&lt;p&gt;时间复杂度：$O((log_2a)^3)$
最大公约数的公式 $gcd(a, b)=gcd(b, a % b)$
证明：设 $d$ 为 $a, b$ 的任意一个约数，那么 $d$ 也为 $b, a% b$ 的约数。显然，$d$ 是 $b$ 的约数成立，由于 $a % b = a - \lfloor \frac{a}{b} \rfloor * b$ ，根据 $(a \pm b) % d = (a % d \pm b % d) % d$ 。因此，$d$ 也是 $a % b$ 的约数。那么如果 $d^\prime$ 是 $gcd(a,b)$ 的最大公约数，则 $d^\prime$ 也是 $gcd(b,a % b)$ 的最大公约数。
设 $d$ 为 $b, a % b$ 的任意一个约数，那么 $d$ 也是 $a, b$ 的约数。显然， $d$ 是 $b$ 的约数成立，由于 $a = a % b + \lfloor \frac{a}{b} \rfloor * b$ 。根据同余公式，则 $d$ 也是 $a$ 的约数。同理，$b, a % b$ 的最大公约数也是 $a, b$ 的最大公约数。
综上：$gcd(a,b)=gcd(b,a%b)$。&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LL gcd(LL a, LL b) {
    return b ? gcd(b, a % b) : a;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;拓展
$gcd(a_1,a_2,a_3,\cdots,a_n)=gcd(a_1,a_2-a_1,\cdots,a_n-a_{n-1})$
根据此公式，序列的区间 $gcd$ 等价于序列差分数组的 $gcd$&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;更相减损法&lt;/h5&gt;
&lt;p&gt;时间复杂度 $max(a,b)$
$gcd(a,b)=gcd(a, a-b)=gcd(b, a-b)$
因此一般使用 $stein$ 算法&lt;/p&gt;
&lt;h5&gt;stein 算法&lt;/h5&gt;
&lt;p&gt;如果 $a,b$ 都是偶数 $gcd(a, b) = 2*gcd(a/2,b/2)$
如果 $a,b$ 其中一个为奇数，根据 $gcd(kx,y)=gcd(x,y)$ 如果 $x,y$ 互质，因此此处 $k$ 取 $2$，则有 $gcd(a,b)=gcd(a/2,b)$，这里适用高精度，但是建议py&lt;/p&gt;
&lt;h4&gt;裴蜀定理&lt;/h4&gt;
&lt;p&gt;如果 $a$ 和 $b$ 均为整数，则有整数 $x$ 和 $y$ 使 $ax+by=gcd(a,b)$&lt;/p&gt;
&lt;p&gt;因此，有推论：当 $a,b$ 互素时，有整数 $x$ 和 $y$ 使得 $ax+by=1$&lt;/p&gt;
&lt;h2&gt;线性丢番图方程&lt;/h2&gt;
&lt;p&gt;根据[[#裴蜀定理]] ，方程 $ax+by=c$ 有整数解的充要条件是 $gcd(a,b)$ 是 $c$ 的倍数。假设 $(x_0,y_0)$ 是一组特解，所有通解可以表示为 $x=x_0+(b/d)n,y=y_0-(a/d)n$，$n$ 为任意整数&lt;/p&gt;
&lt;h3&gt;扩展欧几里得求二元线性丢番图的特解&lt;/h3&gt;
&lt;p&gt;首先利用扩展欧几里得求方程 $ax+by=gcd(a,b)$ 的特解，代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LL exgcd(LL a, LL b, LL &amp;amp;x, LL &amp;amp;y) {
    if (b == 0) {
        x = 1, y = 0;
        return a;
    }
    LL d = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return d;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么可以根据这个来求 $ax+by=c$ 的特解&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;判断 $c$ 是否整数 $gcd(a,b)$&lt;/li&gt;
&lt;li&gt;求$ax+by=gcd(a,b)$ 的特解 $(x_0, y_0)$&lt;/li&gt;
&lt;li&gt;在 $ax_0+by_0=gcd(a,b)$ 两边乘 $c / gcd(a,b)$ ，可得特解为 $(x_0^\prime, y_0^\prime)=(x_0 * c / gcd(a,b), y_0 * c / gcd(a,b)$&lt;/li&gt;
&lt;li&gt;通解为 $x=x_0^\prime + (b/d)n, y=y_0^\prime-(a/d)n$，其中 $d=gcd(a,b)$&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;同余&lt;/h2&gt;
&lt;p&gt;$a\equiv b (\text{mod} \  m)$  称为 $a$ 和 $b$ 同余
同余可以转化成等式存在 $k$，使得 $a = b + mk$&lt;/p&gt;
&lt;h3&gt;一元线性同余方程&lt;/h3&gt;
&lt;p&gt;求解 $ax\equiv b (\text{mod} \  m)$ ，这个同余方程等价于二元丢番图方程，方程有解&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定理&lt;/strong&gt;
设 $a,b$ 和 $m$ 是整数，$m&amp;gt;0,gcd(a,m)=d$。若 $d$ 不能整除 $b$，则 $ax \equiv b (\text{mod} \ m)$ 无解；若 $d$ 能够整除 $b$ ，则 $ax \equiv b (\text{mod} \ m)$ 有 $d$ 个模 $m$ 不同余的解&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推论&lt;/strong&gt;
$a,m$ 互素，线性同余方程 $ax \equiv b (\text{mod} \ m)$ 有&lt;strong&gt;唯一的模 $m$ 不同余&lt;/strong&gt;的解。&lt;/p&gt;
&lt;h3&gt;逆&lt;/h3&gt;
&lt;p&gt;给定整数 $a$，&lt;strong&gt;且满足 $gcd(a,m)=1$&lt;/strong&gt;，则 $ax \equiv 1 (\text{mod} \ m)$ 的一个解为 $a$ 模 $m$ 的逆，记为 $a^{-1}$&lt;/p&gt;
&lt;h4&gt;求逆&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;[[#费马小定理]]，$m$ 是素数，且 $a,m$ 互素，求 $a^{m-2} % m$&lt;/li&gt;
&lt;li&gt;扩展欧几里得，$a,m$ 互素&lt;/li&gt;
&lt;li&gt;递推求逆 $i=1,inv[i]=1;$
$i&amp;gt;1$ 时，假设 $p/i=k$，余数是 $r$，则 $k*i+r\equiv 0 (\text{mod} \ p)$
因此 $r^{-1}i^{-1}ki+r^{-1}i^{-1}r\equiv 0 (\text{mod} \ p)$  等价于 $r^{-1} k + i^{-1} \equiv 0 (\text{mod} \ p)$
移项 $i^{-1} \equiv -r^{-1}k(\text{mod} \ p)$ 等价于 $i^{-1} \equiv (p-p/i)r^{-1}(\text{mod} \ p)$
$inv[i]=(p-p/i)*inv[p%i] % p$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;逆可以用来求解同余方程，若$gcd(a,m)=1$，上边的同余方程可以适用下面的式子来求解 $x\equiv a^{-1}b(\text{mod} \ m)$，在模 $m$ 下有唯一解&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;定理&lt;/strong&gt; 设 $p$ 是素数，正整数 $a$ 是其自身的逆，当且仅当 $a\equiv 1 (\text{mod} \ p)$ 或者 $a\equiv -1 (\text{mod} \ p)$  正如[[#二次探测定理]]&lt;/p&gt;
&lt;p&gt;除法取模 $(a/b) % m = (a/b * b*b^{-1}) % m =  (ab^{-1}) % m$&lt;/p&gt;
&lt;h3&gt;中国剩余定理&lt;/h3&gt;
&lt;p&gt;假设有 $m_1,m_2,\cdots, m_n$ &lt;strong&gt;两两互质&lt;/strong&gt; ，且
$$
x \equiv a_i(mod \ m_i)
$$
那么该方程有一个通用解
设 $M = m_1 * m_2 * \cdots * m_n$ , $M_i = M / m_i$ 因此 $M_i$ 一定与 $m_i$ 互质，故M, M设 $M_i^{-1}$ 为 $M_i$ 模 $m_i$ 的逆元
因此可以构造答案
$$
x \equiv a_1 * M_1 * M_1^{-1} + a_2 * M_2 * M_2^{-1} + \cdots + a_n * M_n * M_n^{-1} (\text{mod} \ M)
$$
可以验证此 $x$ 成立&lt;/p&gt;
&lt;h4&gt;拓展&lt;/h4&gt;
&lt;p&gt;中国剩余定理的拓展，因为剩余定理求解公式需要满足两两互质的强限制，因此需要更一般的解法&lt;/p&gt;
&lt;p&gt;解法:
由于 $x \equiv a_i(mod \ m_i)$ 等价于 $x = k_i * m_i + a_i$ , 其中 $k_i$ 是系数
我们可取前两个等式考虑即 $x = k_1 * m_1 + a_1$ , $x = k_2 * m_2 + a_2$ ，合并得
$$
k_1 * m_1 + a_1 = k2 * m_2 + a_2
$$
整理得
$$
k_1 * m_1 - k2 * m_2 = a_2 - a_1
$$
根据裴蜀定理, 要想有整数解，则 $a_2 - a_1$ 为 $(m_1,m_2)$ 的倍数
故可根据扩展欧几里得求出特解 $k_1^{\prime}, k_2^{\prime}$
因此可得通解 ${k_1^{\prime}&lt;em&gt;\frac{c}{d} + k * \frac{m_2}{(m_1,m_2)}, -k_2^{\prime}&lt;/em&gt;\frac{c}{d} + k * \frac{m_1}{(m_1,m_2)}}$，其中 k 是系数
带入到求解 $x$ 的式子中得
$$
x = (k_1^{\prime}&lt;em&gt;\frac{c}{d} + k * \frac{m_2}{(m_1,m_2)}) * m_1 + a_1
$$
整理得
$$
x = [m_1,m_2] * k + a_1 + k_1^{\prime} * m_1&lt;/em&gt;\frac{c}{d}
$$
可以发现，这是一个与 $x = k_i * m_i + a_i$ 类似的式子，因此，便可以通过这种方式求解 $x$
细节注意：$a_2 - a_1$ 的计算可以写成 $(a_2 - a_1 % m_2 + m_2) % m_2$；计算 $k_1^{\prime} * m_1*\frac{c}{d}$ 分开求，可能爆 &lt;code&gt;long long&lt;/code&gt;；最后计算 $a_1+k_1^\prime * m_1$ 最后对 $[m_1,m_2]$ 取模；核心思想在于在这个过程中，让值变得更小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LL calc() {
    
    LL a1 = a[1], m1 = m[1];
    for (int i = 2; i &amp;lt;= n; i ++ ) {
        LL a2 = a[i], m2 = m[i];
        LL a = a1, b = a2, c = (m2 - m1 % a2 + a2) % a2;
        LL x, y;
        LL d = exgcd(a, b, x, y);
        if (c % d) return -1;
        // b0 的计算也可以龟速乘
        LL k = b / d, b0 = (c / d * x % k + k) % k;
        LL ans = a1 * k;
        // 下面这行可能爆longlong 不行就龟速乘
        m1 = (a1 * b0 % ans + m1) % ans;
        a1 = ans;
    }
    return m1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;BSGS 算法&lt;/h2&gt;
&lt;p&gt;问题描述：若 $gcd(a,p)=1$，求 $a^t \equiv b (\text{mod} \ p)$ 求最小非负正整数解 $t$&lt;/p&gt;
&lt;p&gt;根据欧拉定理，$a^{\phi(p)} \equiv 1 (\text{mod} \ p)$ ，$a^t \equiv a^{t \  \text{mod} \ \phi(p)} (\text{mod} \ p)$
因此，最小的 $t \in [0,\phi(p)-1]$
在计算的时候可以稍微扩大成 $[0,p]$，避免计算欧拉函数&lt;/p&gt;
&lt;p&gt;枚举是利用分块的思想，首先将这个区间分成 $\sqrt p$ 段，令 $k=\lfloor \sqrt p \rfloor + 1 \gt \sqrt p$    ，$t = kx - y$ ，为了让 $t$ 能够取到 $[0,p]$ 中的所有数，可以设置 $x \in [1,k], y \in [0,k-1]$ ，这样 $t \in [1,k^2]$ ，因为 $k^2 \gt p$ 。&lt;strong&gt;注意，0特判即可&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原式可变形为 $a^{kx} \equiv b \cdot a^y (\text{mod} \ p)$，也就是说，枚举 $x$ 的时候，判断是否有对应的 $y$ 使其同余 $p$ 相等&lt;/p&gt;
&lt;h2&gt;欧拉函数&lt;/h2&gt;
&lt;p&gt;$1-N$ 中与 $N$ 互质的数的个数被称为欧拉函数，记为 $\phi(N)$
欧拉函数的计算公式为：
$$
\phi(N) = N \times \frac{p_1 - 1}{p_1} \times \cdots \times \frac{p_k - 1}{p_k}
$$
这里提到另外一个本人想到的公式，设 $p$ 为小于 $N$ 的约数，记 $\phi^p(N)$ 为 $x \in [1,N]$ 中 $gcd(x, N) = p$ 的 $x$ 的个数。其计算公式为：
$$
\phi^p(N) = \phi(\frac{N}{p})
$$
欧拉函数计算公式证明：
证明思路是计算 $1-N$ 中有多少数是 $N$ 的合数，最后用 $N$ 减去这个结果即可。那么 $1-N$ 中有多少是 $N$ 的合数可以根据唯一分解定理计算，一个数是 $N$ 的合数，一定是 $p_1,p_2, \cdots ,p_k$ 的倍数，因此可以通过计算 $1-N$ 中有多少数是这些数的倍数，减掉即可。即：$N - (\frac{N}{p_1} + \cdots + \frac{N}{p_k})$ 。但是，这样还不行，因为像 $p_1 \times p_2$ 的倍数其实被删掉了两次，要重新加回来，这也是通过容斥原理来计算。因此，最终得到的公式为上述公式。&lt;/p&gt;
&lt;h3&gt;线性筛求欧拉函数&lt;/h3&gt;
&lt;p&gt;欧拉函数可以用筛素数的写法来计算欧拉函数，线性筛的本质是用一个数的最小质数将其筛掉，那么便可以思考一个数的欧拉函数值是否可以通过其最小质数和其相应约数计算得到。&lt;/p&gt;
&lt;p&gt;首先，对于 $n$ 为质数，那么 $\phi(n)$ 为 $n-1$。&lt;/p&gt;
&lt;p&gt;然后思考线性筛的过程，如果 $i % primes[j] == 0$ 时，会发生什么，对于 $i&lt;em&gt;primes[j]$ 来说，$primes[j]$ 就是它的最小质数，但是事实上此时 $primes[j]$ 是$i$ 的约数了，根据欧拉函数与指数没有关系，假设，$\phi(i) = i * x$，那么 $\phi(i&lt;/em&gt;primes[j]) = i * primes[j] * x = primes[j] * phi(i)$。&lt;/p&gt;
&lt;p&gt;如果 $i % primes[j] != 0$ 时，则说明 $i$ 的质因数分解中原来没有 $primes[j]$，此时 $phi(i) = i&lt;em&gt;x$，那么 $phi(i&lt;/em&gt;primes[j])=i * primes[j] * \frac{primes[j] - 1}{primes[j]} * x = (primes[j] - 1) * phi(i)$ 。&lt;/p&gt;
&lt;p&gt;核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;oid get_euler(int n) {

    for (int i = 2; i &amp;lt;= n; i ++ ) {
        if (!st[i]) {
            primes[cnt ++ ] = i;
            phi[i] = i - 1;
        }
        for (int j = 0; i &amp;lt;= n / primes[j]; j ++ ) {
            st[i * primes[j]] = true;
            if (i % primes[j] == 0) {
                phi[i * primes[j]] = phi[i] * primes[j];
                break;
            }
            phi[i * primes[j]] = phi[i] * (primes[j] - 1);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;欧拉定理&lt;/h3&gt;
&lt;p&gt;设 $a,m \in N^+$, 且 $gcd(a,m)=1$，$\phi(x)$ 为欧拉函数，有
$$
a^{\phi(m)} \equiv 1 (\text{mod} \ m)
$$&lt;/p&gt;
&lt;h3&gt;欧拉降幂&lt;/h3&gt;
&lt;p&gt;$$
a^b \equiv&lt;br /&gt;
\begin{cases}&lt;br /&gt;
a^{b % \phi(m)} &amp;amp;\quad gcd(a,m)=1\&lt;br /&gt;
a^b &amp;amp;\quad b &amp;lt; \phi(m) \&lt;br /&gt;
a^{b % \phi(m) + \phi(m)} &amp;amp;\quad b \ge \phi(m) \&lt;br /&gt;
\end{cases}  \qquad (\text{mod \ m})
$$&lt;/p&gt;
&lt;h2&gt;莫比乌斯反演&lt;/h2&gt;
&lt;h3&gt;莫比乌斯函数&lt;/h3&gt;
&lt;p&gt;根据[[#唯一分解定理]]&lt;/p&gt;
&lt;p&gt;若 $F(n)=\sum_{n | d} f(d)$，则 $f(n)=\sum_{n|d} \mu (\frac{d}{n})F(d)$&lt;/p&gt;
&lt;h1&gt;线性代数&lt;/h1&gt;
&lt;h2&gt;矩阵快速幂&lt;/h2&gt;
&lt;p&gt;这里提供矩阵快速幂的模板。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct matrix { int m[N][N]; };

matrix operator * (const matrix&amp;amp; a, const matrix&amp;amp; b) {
    matrix c;
    memset(c.m, 0, sizeof c.m);
    for (int i = 0; i &amp;lt; N; i ++ )
        for (int j = 0; j &amp;lt; N; j ++ )
            for (int k = 0; k &amp;lt; N; k ++ )
                c.m[i][j] = (c.m[i][j] + 1ll * a.m[i][k] * b.m[k][j] % mod) % mod;
    return c;
}

matrix pow_matrix(matrix a, int k) {
    matrix ans;
    memset(ans.m, 0, sizeof ans);
    for (int i = 0; i &amp;lt; N; i ++ ) ans.m[i][i] = 1;
    while (k) {
        if (k &amp;amp; 1) ans = ans * a;
        k &amp;gt;&amp;gt;= 1;
        a = a * a;
    }
    return ans;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个模板是相当不错的，矩阵乘里面的 $N$, mod 可以不是常量，可以在事后输入。&lt;/p&gt;
&lt;h3&gt;矩阵快速幂的应用&lt;/h3&gt;
&lt;p&gt;对于 $f(n)=f(1)+f(2)+\cdots+f(n-1) + c$ 这样的递推式子，容易构造矩阵
$$
\begin{pmatrix}
f(n) &amp;amp; f(n-1) &amp;amp; \cdots &amp;amp; f(2) \
\end{pmatrix} =
\begin{pmatrix}
f(n-1) &amp;amp; f(n-2) &amp;amp; \cdots &amp;amp; f(1) \
\end{pmatrix} A
$$
其中，矩阵 $A$  就是根据递推式构造的矩阵
有的时候，递推式就是根据动态规划转移式子得来，有的时候隐藏的很深，需要去发现去设计&lt;/p&gt;
&lt;h2&gt;高斯消元&lt;/h2&gt;
&lt;p&gt;在高斯消元的过程中，我们需要对判断最终的结果是唯一解？多解还是无解？这里有一个通用的判断方式（$n,m$关系未知，$m$ 行 $n$ 列）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	// 首先判断在进行了多次处理后 最后 row ~ m 行的最后一列是否全为 0，不是则为无解
    for (int i = row; i &amp;lt;= m; i ++ )
        if (a[i][n + 1]) return 0;
    // row - 1 表示秩的大小，显然秩为 n 的时候为唯一解
    if (row == n + 1) return 1;
    return 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度 $O(nm^2)$，其中 $n,m$ 分别为行列&lt;/p&gt;
&lt;h3&gt;高斯消元求解线性方程组&lt;/h3&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
输出解 注意 0 的判断
for (int i = 1; i &amp;lt;= n; i ++ )
    printf(&quot;%.3f &quot;, fabs(a[i][n + 1]) &amp;lt; eps ? 0.000 : a[i][n + 1]);
*/
// 0 无解 1 唯一解 2 无穷解
int guass() {
    
    int row, col;
    for (col = 1, row = 1; col &amp;lt;= n; col ++ ) {
        int t = row;
        for (int i = row + 1; i &amp;lt;= n; i ++ )
            if (fabs(a[i][col]) &amp;gt; fabs(a[t][col]))
                t = i;
        
        if (fabs(a[t][col]) &amp;lt; eps) continue;
        for (int i = col; i &amp;lt;= n + 1; i ++ ) swap(a[t][i], a[row][i]);
        for (int i = n + 1; i &amp;gt;= col; i -- ) a[row][i] /= a[row][col];
        
        for (int i = 1; i &amp;lt;= n; i ++ )
            if (i != row &amp;amp;&amp;amp; fabs(a[i][col]) &amp;gt; eps) {
                for (int j = n + 1; j &amp;gt;= col; j -- ) a[i][j] -= a[row][j] * a[i][col];
            }
        
        row ++ ;
    }
    if (row &amp;lt;= n) {
        for (int i = row; i &amp;lt;= n; i ++ )
            if (fabs(a[i][n + 1]) &amp;gt; eps) return 0;
        return 2;
    }
    return 1;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;高斯消元求解异或线性方程组&lt;/h3&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int guass() {
    
    int col, row;
    for (col = row = 1; col &amp;lt;= n; col ++ ) {
        int t = row;
        for (int i = row + 1; i &amp;lt;= n; i ++ )
            if (a[i][col] &amp;gt; a[t][col]) t = i;
       
        if (a[t][col] == 0) continue;
        for (int i = col; i &amp;lt;= n + 1; i ++ ) swap(a[t][i], a[row][i]); 
        
        for (int i = 1; i &amp;lt;= n; i ++ )
            if (i != row &amp;amp;&amp;amp; a[i][col] == 1) {
                for (int j = n + 1; j &amp;gt;= col; j -- )
                    a[i][j] ^= a[row][j];
            }
        row ++ ; 
    }

    if (row &amp;lt;= n) {
        for (int i = row; i &amp;lt;= n; i ++ )
            if (a[i][n + 1] &amp;gt; 0) return 0;
        return 2;
    }
    return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;高斯消元求解模运算线性方程组&lt;/h3&gt;
&lt;p&gt;代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注意这里是 模 3 的写法，如果是其他的模，需要求逆
// 以下如果是模 p 的话，则 3 改为 p
void gauss() {

    int col, row;
    for (col = row = 0; col &amp;lt; n; col ++ ) {
        int t = row;
        for (int i = row + 1; i &amp;lt; n; i ++ )
            if (a[i][col] &amp;gt; a[t][col]) t = i;
        if (a[t][col] == 0) continue;//print();
        for (int i = col; i &amp;lt;= n; i ++ ) swap(a[t][i], a[row][i]);
        // 此处需要求逆，其中 2 替换为 a[row][col]对于 p 的逆
        // 这个 if 需要去掉
        if (a[row][col] == 2) {
            for (int i = n; i &amp;gt;= col; i -- ) a[row][i] = (a[row][i] * 2) % 3;
        }
        for (int i = 0; i &amp;lt; n; i ++ )
            if (i != row &amp;amp;&amp;amp; a[i][col]) {
                for (int j = n; j &amp;gt;= col; j -- ) 
                    a[i][j] = (a[i][j] + (3 - a[i][col]) * a[row][j]) % 3;
            }
        row ++ ;
        
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;线性基&lt;/h2&gt;
&lt;h3&gt;迭代构造线性基&lt;/h3&gt;
&lt;p&gt;时间复杂度 $O(nm)$，其中 $m$ 为 $n$ 个数里面的最大位数，在 &lt;code&gt;long long&lt;/code&gt; 范围内等价于 $O(64n)$&lt;/p&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LL base[63]; // 存储线性基
bool zero = false; // 查看是否这组数据里面能否出现 0 

void insert(LL x) {
    
    for (int i = 62; i &amp;gt;= 0; i -- ) {
        if (x &amp;gt;&amp;gt; i == 1) {
            if (base[i] == 0) {base[i] = x; return ;}
            else x = x ^ base[i];
        } 
    }
    zero = true;
}

LL qmax() {
    LL res = 0;
    for (int i = 62; i &amp;gt;= 0; i -- )
        res = max(res, res ^ base[i]);
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>