几个关键的核心思想 | 例如 |
---|---|
充分使用当前已知条件,去掉重复无用遍历 | 递归-DP-回溯 |
递归
搜索时核心思想是枚举和回溯
DFS(Depth First Search)
//伪代码
class dfs(状态A){
if(A不合法)return ERR;
if(A==目标)return TAR;
else dfs(A的子节点);
}
N红皇后问题
国际象棋,一个皇后对角线、同行、同列上都不能有其他皇后
用一个2n大小的数组存储主副对角线和同列状态
主对角线相同 有x+y相同 //state[1][x+y]都是同一对角线
副对角线相同 有x+n-y相同
int cnt = 0; //可行解的个数
int *pos = new int[n]; //记录cur行红皇后的位置
vector<vector<bool>> state(3,vector<bool>(2n,true));
//存储状态:true=可以放;false=不可以放;
//[0][j]=同列;[1][j]=主对角线;[2][j]=副对角线
void dfs(int cur){ //当前搜索的行
if(cur==n){ //搜索到了第n+1行,即得到一种可能性
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(pos[i]==j)cout<<'x'; //当前可行解当前行上红皇后的位置
else cout<<'o'; //不能放的位置
}
cout<<endl;
}
cnt++;
}
else{ //cur<n;没有搜索到n+1行;搜索没有结束
for(int j=0;j<n;j++){ //列举当前行的可能性
if(state[0][j]&&state[1][cur+j]
&&state[2][cur+n-j]) //同列、对角线没有,可放
state[0][j]=false; //更新状态
state[1][cur+j]=false;
state[2][cur+n-j]=false;
pos[cur] = j; //记录当前行皇后的位置
dfs(cur+1); //搜索下一行的可能性
state[0][j]=true; //还原状态,开始另一种可能性
state[1][cur+j]=true;
state[2][cur+n-j]=true;
}
}
}
24点
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
bool dfs(float* a,int n,char *op) {
if (n >= 3) {
float tmp = a[0];
for (int i = 0; i < 3; i++) {
switch(op[i]){
case'+':tmp += a[i + 1]; break;
case'-':tmp -= a[i + 1]; break;
case'*':tmp *= a[i + 1]; break;
case'/':tmp /= a[i + 1]; break;
}
}
cout << a[0]<<op[0]<< a[1]<<op[1]<< a[2]<<op[2]<< a[3]<<'='<<tmp<< endl;
if (tmp == 24)
return true;
else
return false;
}
else {
op[n] = '+';
if(dfs(a, n + 1, op))return true;
op[n] = '-';
if (dfs(a, n + 1, op))return true;
op[n] = '*';
if (dfs(a, n + 1, op))return true;
op[n] = '/';
if (dfs(a, n + 1, op))return true;
return false;
}
}
int main() {
float a[4] = { 0,0,0,0 };
while (cin >> a[0])
{
for (int i = 1; i < 4; i++)
cin >> a[i];
float tmp = 0;
for (int i=4;i>0;i--)
for (int j = 1; j < i; j++)
if (a[j - 1] > a[j]) {
tmp = a[j - 1];
a[j - 1] = a[j];
a[j] = tmp;
}
char op[3] = { '\0' };
bool f = false;
while (next_permutation(a, a + 4))
{
f = dfs(a, 0, op);
if (f)break;
}
cout << (f ? "true" : "false") << endl;
}
return 0;
}
数独
在存在多解的情况下,使用暴力枚举很可能有 无法负担 的时间代价
此时应该使用回溯,利用当前条件尽快排除不可能的当前分支状况,找到一个可行方向
即 先判断合不合理,再递归下去
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
bool check(int (&a)[9][9], int pos)
{
int r = pos / 9, c = pos % 9;
int count[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
for (int i = 0; i < 9; i++)
{
if (a[r][i] != 0 && count[a[r][i]] != 0)
{
return false;
}
count[a[r][i]]++;
}
fill(count, count + 10, 0);
for (int i = 0; i < 9; i++)
{
if (a[i][c] != 0 && count[a[i][c]] != 0)
{
return false;
}
count[a[i][c]]++;
}
fill(count, count + 10, 0);
int ri = r / 3, ci = c / 3;
for (int i = 0; i < 9; i++)
{
if (a[ri * 3 + i / 3][ci * 3 + i % 3] != 0 && count[a[ri * 3 + i / 3][ci * 3 + i % 3]] != 0)
{
return false;
}
count[a[ri * 3 + i / 3][ci * 3 + i % 3]]++;
}
return true;
}
bool backtrack(int (&a)[9][9], int pos)
{
if (pos == 81)
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
cout << a[i][j];
}
cout << endl;
}
return true;
}
int r = pos / 9, c = pos % 9;
if (a[r][c] != 0)
{
return backtrack(a, pos + 1);
}
for (int i = 1; i <= 9; i++)
{
a[r][c] = i;
if (check(a, pos))
{
if (backtrack(a, pos + 1))
{
return true;
}
}
a[r][c] = 0;
}
return false;
}
int main()
{
int a[9][9];
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
cin >> a[i][j];
}
}
backtrack(a, 0);
return 0;
}
BFS(Breadth First Search)
//伪代码
class bfs(){
Q.push_back(head); //一个队列存储每一层的兄弟节点
while(!Q.empty()){
tmp=Q.front();
Q.pop();
if(tmp非法)continue;
else if (tmp==目标)return TAR;
else Q.push_back(tmp子节点);
}
}
迷宫找出路(deque)
#include<iostream>
#include<vector>
#include<deque>
using namespace std;
int main() {
int n = 0, m = 0, k = 0, x = 0, y = 0;
cin >> n >> m >> k;
vector<vector<int>> map(n, vector<int>(m, 0));
vector<deque<int>>tmpx(2, deque<int>()), tmpy(2, deque<int>());
int cur = 0,next = 1;
for (int i = 0; i < k; i++) {
cin >> x >> y;
map[x][y] = 2; //障碍=2
}
tmpx[cur].push_back(0);
tmpy[cur].push_back(0);
int step = 0;
bool find = false;
while (!tmpx[0].empty() || !tmpx[1].empty()) {
if ((tmpx[cur].front() != n - 1) || (tmpy[cur].front() != m - 1)) {
map[tmpx[cur].front()][tmpy[cur].front()] = 1;//走过 //障碍=2 走过=1
if (tmpx[cur].front() + 1 < n && map[tmpx[cur].front() + 1][tmpy[cur].front()] == 0) { //向下可行
tmpx[next].push_back(tmpx[cur].front() + 1);
tmpy[next].push_back(tmpy[cur].front());
}
if (tmpx[cur].front() - 1 >= 0 && map[tmpx[cur].front() - 1][tmpy[cur].front()] == 0) { //向上可行
tmpx[next].push_back(tmpx[cur].front() - 1);
tmpy[next].push_back(tmpy[cur].front());
}
if (tmpy[cur].front() - 1 >= 0 && map[tmpx[cur].front()][tmpy[cur].front() - 1] == 0) { //向左可行
tmpx[next].push_back(tmpx[cur].front());
tmpy[next].push_back(tmpy[cur].front() - 1);
}
if (tmpy[cur].front() + 1 < m && map[tmpx[cur].front()][tmpy[cur].front() + 1] == 0) { //向右可行
tmpx[next].push_back(tmpx[cur].front());
tmpy[next].push_back(tmpy[cur].front() + 1);
}
tmpx[cur].pop_front(); //去掉当前步
tmpy[cur].pop_front();
if(tmpx[cur].empty()) {//当前步探测完毕
step++;
int tmp = cur;
cur = next;
next = tmp;
}
}
else
{
find = true;
cout << step << endl;
break;
}
}
if (find == false)cout << 0 << endl;
return 0;
}
动态规划(DP)
DP可以看作是对递归枚举的剪枝优化,从下至上只搜索可能的分支
需要满足无后向性:之后的状态并不影响之前的状态
1.找状态量
2.找状态方程(大问题拆成小问题)
3.确定初始和边界
4.确定顺序
最长递增子序列
Input:
[2,1,4,3,1,5,6],7
Output:
4
方法一: O(n2)
核心 if(A[j]<A[i])B[i]=max(B[i],B[j]+1);
class AscentSequence {
public:
int findLongest(vector<int> A, int n) {
// write code here
int maxn=0;
int* B=new int[n];
for(int i=0;i<n;i++)B[i]=1;
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(A[j]<A[i])B[i]=max(B[i],B[j]+1);
}
if(maxn<B[i])maxn=B[i];
}
return maxn;
}
};
方法二:O(NlogN)
核心 B为有序数列,存储每个[]长度子序列中最后一位,B[findp(B, A[i], end)] = A[i]; 更新小于A[i]的最大的最后一位
int findp(int B[], int Ai, int end) {
int start = 0, mid = 0;
while (start < end) {
mid = (start + end) / 2;
if (B[mid] < Ai)start = mid + 1;
else if (B[mid] > Ai)end = mid;
else return mid;
}
if (end <= 0)return 0;
else return end;
}
int findLongest(vector<int> A, int n) {
int* B = new int[n]();
B[0]=A[0];
int end = 0;
for (int i = 0; i < n; i++) {
if (B[end] < A[i])
B[++end] = A[i];
else if (B[end] > A[i])
B[findp(B, A[i], end)] = A[i];
}
return end + 1;
}
最长公共子序列
Input:
“1A2C3D4B56”, 10, “B1D23CA45B6A”, 12
Output:
6
A[i]与B[j]的重合,状态改变,取决于A[i-1],B[j-1]的状态
class LCS {
public:
int findLCS(string A, int n, string B, int m) {
// write code here
int table[n + 1][m + 1];
for(int i = 0;i <= n;++i)table[i][0] = 0;
for(int i = 0;i <= m;++i)table[0][i] = 0;
for(int i = 0;i < n;++i){
for(int j = 0;j < m;++j){
if(A[i] == B[j])
table[i + 1][j + 1] = table[i][j] + 1;
else {
table[i + 1][j + 1] = max(table[i][j + 1],table[i + 1][j]);
}
}
}
return table[n][m];
}
最小编辑代价
c0,c1,c2分别为插入、删除和修改操作的代价,求将A变为B的最小代价
Input:
“abc”,3,“adc”,3,5,3,100
Output:
8
B[j] ⇐ A[i]删除所有元素(i*c1) + 空字符插入元素到B[j](j*c0)
min(dp[i-1][j] + c1, dp[i][j-1] + c0)
A[i-1]=B[j-1] 则B[j]可以直接由A[i]‘改’+c2,或A[i]删光+增添至B[j]即dp[i][j]
int findMinCost(string A, int n, string B, int m, int c0, int c1, int c2)
{
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
for (int j = 0; j <= m; j++)
dp[0][j] = j*c0;
for (int i = 0;i <= n; i++)
dp[i][0] = i*c1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++){
dp[i][j] = min(dp[i-1][j] + c1, dp[i][j-1] + c0);
if (A[i-1] == B[j-1])
dp[i][j] = min(dp[i-1][j-1], dp[i][j]);
else
dp[i][j] = min(dp[i-1][j-1] + c2, dp[i][j]);
}
return dp[n][m];
}
字符串交错组成
C是否由A和B原序交错组成
Input:
“ABC”,3,“12C”,3,“A12BCC”,6
Output:
true
bool chkMixture(string A, int n, string B, int m, string C, int v) {
if(n+m!=v)return false;
vector<vector<bool>> dp (n+1,vector<bool>(m+1,false));
int a=0;
int b=0;
dp[0][0] = true;
for(int i=0;i<v;i++){
if(A[a] == C[i] && dp[a][b] == true){
a++;
dp[a][b] = true;
}
if(B[b] == C[i] && dp[a][b] == true){
b++;
dp[a][b] = true;
}
}
return dp[n][m];
}
存在多种可能性,验证某状态的可能性 ⇒ if(上一状态==true&&当前状态==true)DP[当前]=true;
最小起点代价
吉比特18年A卷
G社正在开发一个新的战棋类游戏,在这个游戏中,角色只能向2个方向移动:右、下。移动需要消耗行动力,游戏地图上划分M*N个格子,当角色移动到某个格子上时,行动力就会加上格子上的值K(-100~100),当行动力<=0时游戏失败,请问要从地图左上角移动到地图右下角至少需要多少起始行动力,注意(玩家初始化到起始的左上角格子时也需要消耗行动力
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
int m, n;
cin >> m >> n;
vector<vector<int>> cost(m + 1, vector<int>(n + 1, 0));
vector<vector<int>> arr(m + 1, vector<int>(n + 1, -1000));
arr[0][1] = arr[1][0] = 0;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
cin >> arr[i + 1][j + 1];
for (int i = 1; i < m + 1; i++)
for (int j = 1; j < n + 1; j++) {
arr[i][j] = arr[i][j] + max(arr[i - 1][j], arr[i][j - 1]);
if (max(arr[i - 1][j], arr[i][j - 1]) == arr[i - 1][j])
if (arr[i][j] < cost[i - 1][j])cost[i][j] = arr[i][j];
else cost[i][j] = cost[i - 1][j];
else
if (arr[i][j] < cost[i][j-1])cost[i][j] = arr[i][j];
else cost[i][j] = cost[i][j-1];
}
cout << 1-cost[m][n];
return 0;
}
背包问题
01背包
N件物品代价Ci价值Wi放入容量为V 的背包,将哪些物品装入背包可使价值总和最大
状态转移方程:
f
[
i
,
v
]
=
m
a
x
{
f
[
i
−
1
,
v
]
,
f
(
i
−
1
,
v
−
C
i
)
+
W
i
}
f[i,v]=max\{f[i-1,v],f(i-1,v-C_i)+W_i\}
f[i,v]=max{f[i−1,v],f(i−1,v−Ci)+Wi}
转换为动态规划
int f[I+1][V+1]={0};
int val[I+1]={0,...}; //价值
int cst[I+1]={0,...}; //代价
for(int v=0;v<V+1;v++)
for(int i=0;i<I+1;i++)
f[i][v]=0;
for(int i=0;i<I+1;i++)
for(int v=0;v<V+1;v++){
if(cst[i]>v) //放不下
f[i][v]=f[i-1][v];
else{
f[i][v]=max(f[i-1][v-cst[i]]+val[i],f[i-1][v]);
// max(腾出cst[i]空间放下i获得的最大收益,不放i的最大收益)
}
}
return f[I+1][V+1];
递归会出现重复的子问题,是一种暴力枚举
而动态规划使用数组记录子问题的解,因此再次查询时直接读取,避免了再次搜索重复子问题
核心思想:不选(0)还是选(1)
恰好填充
要求正好填满所有格子,对不能存在的状态(空格子)赋惩罚项
−
∞
-\infty
−∞表示非法,使得
完全背包问题
物品不限量可重复装入
F
(
i
,
v
)
=
m
a
x
{
F
(
i
−
1
,
v
−
k
C
1
)
+
k
W
i
}
F(i,v)=max\{F(i-1,v-kC_1)+kW_i\}
F(i,v)=max{F(i−1,v−kC1)+kWi} 此时每种情况还要尝试k次重复装入,复杂度为
O
(
N
V
∑
V
c
i
)
O(NV\sum{\frac{V}{c_i}})
O(NV∑ciV)
首先排除
C
j
≥
C
i
C_j\geq C_i
Cj≥Ci &&
W
j
≤
W
i
W_j\leq W_i
Wj≤Wi这样大又没价值的物品
将每种物品增倍为
2
0
C
i
,
2
1
C
i
,
2
2
C
i
.
.
.
2
k
−
1
C
i
≤
V
2^0C_i,2^1C_i,2^2C_i... 2^{k-1}C_i \leq V
20Ci,21Ci,22Ci...2k−1Ci≤V 这k种物品
//小米笔试19-9-6
//小米之家多种商品不同价格不限量,花光N元,最多买几件,最少买几件?
//输入产品种类数量 \n产品价格\n产品价格...\n 预算N
//输出最多/最少件数,不凑整-1
//示例
//2
//500
//1
//1000
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
//S2能否通过S1删除得到
int main() {
int n = 0, m = 0;
cin >> n;
vector<int> p(n, 0);
for (int i = 0; i < n; i++)
cin >> p[i];
cin >> m;
vector<int>c(1,0), v(1,0); //cost - value
for(int i=0;i<n;i++)
for (int k = 0; pow(2, k) * p[i] <= m; k++) {
v.push_back(pow(2, k));
c.push_back(pow(2, k) * p[i]);
}
//转换为01背包问题
vector<vector<unsigned int>> arr(c.size(), vector<unsigned int>(m+1, 0));
for (int i = 1; i < m + 1; i++)
arr[0][i] = 1000; //空格惩罚项,求最少产品件数设为正无穷
for(int i=1;i<c.size();i++)
for (int j = 1; j < m + 1; j++)
{
if (c[i] > j)arr[i][j] = arr[i - 1][j];
else
arr[i][j] = min(arr[i-1][j], arr[i-1][j-c[i]]+v[i]);
}
//打印dp表格
for (int i = 0; i < c.size(); i++) {
for (int j = 0; j < m + 1; j++)
cout<<arr[i][j]<<"\t";
cout << endl;
}
if (arr[c.size() - 1][m] >= 1000)cout << -1;
else cout<< arr[c.size() - 1][m];
return 0;
}
其他例题
有序二维数组的查找
要完全利用题干信息,arr[0][j]是第j列最小,arr[i][0]是第i行最小
栈的压入、弹出序列
顺序一致= { while(!原栈.empty())if(相等)出栈; if(临时栈.empty())return true;else return false; }
华为机试
输入:3(A) 输出:AAA
输入:2(2{A}2[B])1(C) 输出:AABBAABBC
不考虑非法输入
// 2019.8.30 用时50min I'm so vegetable......
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main() {
string in,com;
cin >> in;
vector<int> bp, n;
int i = 0;
do{
if (in[i] >= '0' && in[i] <= '9')
n.push_back(in[i] - '0');
else if (in[i] == '[' || in[i] == '{' || in[i] == '(') {
com.push_back(in[i]);
bp.push_back(i);
}
else if ((in[i] >= 'a' && in[i] <= 'z') || (in[i] >= 'A' && in[i] <= 'Z')) {
cout << in[i];
}
else if ((in[i] == ']' && com.back() == '[') ||
(in[i] == ')' && com.back() == '(') ||
(in[i] == '}' && com.back() == '{')) {
n.back()--;
if (n.back() != 0)i = bp.back();
else {
bp.pop_back();
com.pop_back();
n.pop_back();
}
}
i++;
} while (i<=in.size()||!com.empty());
return 0;
}
数组分割,最大和最小
原题 Leetcode 410
无后向性:
将 nums[0…i] 分成 j 份时得到当前分割数组最大和的最小值,后面怎么分割不影响这个值
f[i][j] 定义为将 nums[0…i] 分成 j 份时分割数组最大和的最小值
字符串匹配
BF(Brute-Force)
暴力枚举 O(M*N)
回溯
i
=
(
i
−
j
+
1
)
+
1
i=(i-j+1)+1
i=(i−j+1)+1
int BF(string S,string P)
{
int i=0,j=0;
while(i<S.size()&&j<P.size())
{
if(s[i]==P[j]){++i;++j;}
else {i=i-j+2;j=0;}
if(j==P.size())return i-j+1;
}
return -1;
}
KMP(Knuth Morris Pratt)
本质:利用模式串P自身的(对称)结构信息 + 比较过程中得到的信息 O(M+N)
int KMP(string S,string P)
{
//prefix table
vector<int> prefix(P.size(),0);
prefix[0]=0;
int len=0,i=0;
}
TopK
网易雷火
2019.9.15.14:00~18:00
第一题AC
N个矩形,排序
(宽W,高H),优先面积升序
再宽高比
m
i
n
(
W
H
,
H
W
)
min(\frac{W}{H},\frac{H}{W})
min(HW,WH)降序
再W升序
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
class sq
{
public:
float w=0;
float h=0;
float s=0;
float r=0;
sq(float a,float b):w(a),h(b)
{
s=a*b;
if(a!=0&&b!=0)
{
float r1=a/b,r2=b/a;
r=min(r1,r2);
}
else
r=0;
}
};
void mysort(vector<sq> &sqs)
{
for(int i=sqs.size()-1;i>=0;i--)
for(int j=0;j<i;j++)
{
if(sqs[j].s>sqs[j+1].s)
swap(sqs[j],sqs[j+1]);
else if(sqs[j].s==sqs[j+1].s)
{
if(sqs[j].r<sqs[j+1].r)
swap(sqs[j],sqs[j+1]);
else if(sqs[j].r==sqs[j+1].r)
{
if(sqs[j].w>sqs[j+1].w)
swap(sqs[j],sqs[j+1]);
}
}
}
}
int main()
{
int n=0;
cin>>n;
vector<sq> sqs;
for(int i=0;i<n;i++)
{
float a=0.0,b=0.0;
cin>>a;
cin>>b;
sqs.push_back(sq(a,b));
}
mysort(sqs);
for(int i=0;i<n;i++)
cout<< sqs[i].w <<" "<<sqs[i].h<<" ";
return 0;
}
第二题AC
输入N顶点和M个三角形标号(a,b,c),法向量相反则反转点序,最小编号最前
#include<iostream>
#include<vector>
#include<algorithm>
#include<deque>
using namespace std;
class tr
{
public:
bool fine=false;
deque<int> vt;
deque<vector<int>>l;
tr(int a,int b,int c):vt({a,b,c})
{
l.push_back({a,b});
l.push_back({b,c});
l.push_back({c,a});
}
void first_sort()
{
fine=true;
int tmp=min(vt[0],vt[1]);
tmp=min(tmp,vt[2]);
while(tmp!=l.front()[0])
{
vector<int> tmpl=l.front();
l.pop_front();
l.push_back(tmpl);
int tmpv=vt.front();
vt.pop_front();
vt.push_back(tmpv);
}
}
static bool byside(tr &tr1,tr &tr2)
{
int vtcnt=0;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(tr1.vt[i]==tr2.vt[j])vtcnt++;
if(vtcnt==2)
return true;
else
return false;
}
static void turn(tr &untr,tr &tr) //未排序,已排序
{
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(untr.l[i]==tr.l[j])
{
//相邻边同向,三角形异,则重排
swap(untr.vt[0],untr.vt[2]);
untr.l.clear();
untr.l.push_back({untr.vt[0],untr.vt[1]});
untr.l.push_back({untr.vt[1],untr.vt[2]});
untr.l.push_back({untr.vt[2],untr.vt[0]});
untr.first_sort(); //重排序
untr.fine=true;
return;
}
//相邻边异向,三角形同向
untr.first_sort(); //重排序
untr.fine=true;
return;
}
};
int main()
{
int n=0,m=0;
cin>>n>>m;
vector<tr> trs;
deque<tr*> sch;
for(int i=0;i<m;i++)
{
int a=0,b=0,c=0;
cin>>a;
cin>>b;
cin>>c;
trs.push_back(tr(a,b,c));
}
trs[0].first_sort();
//for(int i=0;i<3;i++)
//cout<<trs[0].l[i][0]<<trs[0].l[i][1]<<endl;
sch.push_back(&trs[0]);
while(!sch.empty())
{
deque<tr*> tmpsch=sch;
sch.clear();
for(int cnt=0;cnt<tmpsch.size();cnt++)
for(int i=0;i<m;i++)
{
if(trs[i].fine==false
&&tr::byside(trs[i],*tmpsch[cnt]))
{
tr::turn(trs[i],*tmpsch[cnt]);
sch.push_back(&trs[i]);
}
}
}
for(int i=0;i<m;i++)
cout<< trs[i].vt[0] <<" "<<trs[i].vt[1]<<" "<<trs[i].vt[2]<<endl;
return 0;
}
后面的输出特殊情况能抢10~25%,没抢
第三题看着像背包
做第四题的时候交卷了,奇偶讨论1~
n
\sqrt{n}
n
更正:看了大神的说法,第三题状态压缩dp,第四题欧拉序列。