acwing-算法基础课笔记

这是acwing-算法基础课的笔记

一 基础算法

1. 快速排序

a. 算法思想

该算法主要步骤如下(假设待排序数组为q):

  1. 确定pivot, 一般可选q[l] q[(l+r)/2] q[r]q中的一个随机数

  2. 将所有小于pivot 的值放在它的左侧,所有大于pivot的值放在右侧 (若为降序排列则相反)

  3. 递归处理左半边和右半边子数组

步骤2实现方法

  • 需要额外空间
  1. 开辟两个新数组ab, 将所有小于等于pivot 的元素放到a 中,所有大于pivot 的放到b

  2. ab中的元素重新放回q

  • 无需额外空间
1
2
3
4
5
6
7
8
9
10
11
12
int partition(int q[],int l ,int r,int pivot)
{
int i = l;
int j = r;
while(i<j)
{
do i++; while(q[i]<pivot);
do j--; while(q[j]>pivot);
if(i<j) swap(q[i],q[j]);
}
return j;
}

b. 算法模板

partition 函数与递归综合到一个函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
void quickSort(int q[], int l, int r)
{
if(l>=r) return;
int pivot = q[(l+r)/2], i = l-1 , j = r+1;
while(i<j)
{
do i++; while(q[i]<pivot);
do j--; while(q[j]>pivot);
if(i<j) swap(q[i],q[j]);
}
quickSort(q,l,j);
quickSort(q,j+1,r);
}

2. 归并排序

a. 算法思想

该算法主要步骤如下(假设待排序数组为q):

  1. 确定分界点mid

  2. 递归处理q的左半边q[0...mid]和右半边q[mid...n-1]

  3. 进行归并

b. 算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void mergeSort(int q[], int l, int r)
{
if(l>=r) return;
int mid = l+r>>1;
mergeSort(q,l,mid);
mergeSort(q,mid+1,r)
int* temp = new int[r-l+1];
int i = l,j = mid+1,k = 0;
while(i<=mid && j<=r)
{
if(q[i]<q[j])
{
temp[k++] = q[i++];
}
else
{
temp[k++] = q[j++];
}
}
while(i<=mid)
{
temp[k++] = q[i++];
}
while(j<=r)
{
temp[k++] = q[j++];
}
for(int i=l,j=0;i<=r;i++,j++) q[i] = temp[j];

}

3. 整数二分查找

a. 二分本质

"单调性"是"可二分"的充分非必要条件

b. 算法模板

2个模板

第2个模板加1的原因:若不加1,则会导致mid=l 可能陷入死循环

4. 浮点二分查找

与3有一点不同,即有误差存在(i-j不等于0),且更新时leftright直接等于mid.

5. 高精度计算

a. 加法

算法思想

  1. 将大整数(A B)每位倒序(方便进位)存放在数组中

  2. 按照竖式加法法则计算

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> add(vector<int>& A, vector<int>& B)
{
vector<int> C;
int carry = 0;
for(int i = 0; i<A.size() || i<B.size();i++)
{
if(i<A.size()) carry += A[i];
if(i<B.size()) carry += B[i];
C.push_back(carry % 10); // the digit
carry /= 10; // if carry exists
}
if(carry) C.push_back(1)
return C;
}

b. 减法

算法思想

  1. 将大整数((A B)每位倒序存放在数组中

  2. 判断A B大小,若A>=B则计算A-B, 否则计算 -(B-A)

  3. 按照竖式减法法则计算

  4. 去掉前导0

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool cmp(vector<int>& A, vector<int>& B) // check if A >= B
{
// digits of A is not equal to that in B
if(A.size() != B.size()) return A.size() > B.size()
// else compare from high to low, until digits that are
// not equal to each other
for(int i = A.size()-1; i >= 0; i--)
{
if(A[i] != B[i]) return A[i] > B[i]
}
return true; //A = B
}


vector<int> sub(vector<int>& A, vector<int>& B)
{
// calculate A - B
vector<int> C;
int carry = 0;
for(int i = 0;i<A.size();i++) // A.size() >= B.size()
{
carry = A[i] - carry;
if(i<B.size()) carry -= B[i];
// handles both situations that carry > 0 or carry <= 0
C.push_back((carry+10) % 10);
if(carry < 0) carry = 1;
}
// delete guiding 0s
while(C.size()>1 && C.back() == 0) C.pop_back()
// C.back() refers to the last element in C
return C;

}

c. 乘法

算法思想

  1. 将大整数(A)每位倒序存放在数组中,设小整数为b

  2. 以A的每一位乘b得c, c%10+carry 为该位结果,c/10 为进位carry

  3. 重复2,直至A的每一位遍历结束

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vector<int> mul(vector<int>& A,int b)
{
vector<int> C;
int carry = 0;
for(int i = 0;i<A.size();i++)
{
carry += (A[i] * b);
C.push_back(carry % 10);
carry /= 10;
}
if(carry != 0) C.push_back(carry)
while(C.size() > 1 && C.back() == 0) C.pop_back(); // 去除前导0
return C;
}

d. 除法

算法思想

此图说明一切

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
int divide(vector<int>& A, int b,queue<int>& C)
{
int carry = 0;
for(auto m : A)
{
carry = carry * 10 + m;
C.push(carry / b);
carry %= b;
}
while(C.front() == 0 && C.size() > 1) C.pop(); // 去除前导0
return carry;
}

6. 前缀和

  • 一维

对于一个数组A[1...n],其前缀和数组S[0...n] 的计算公式为:

作用:快速求出原数组中一段数的和,公式为:

  • 二维

S[i][j] 的递推公式:

解释:

快速求出二维数组一方框数的和,公式:

解释:

其本质均为容斥原理

7. 差分

前缀和的逆运算,即已知SA

  • 一维

递推公式:

作用:快速由S还原A, 解题时一般先差分得到新数组,再在得到的新数组上进行 的区间加减操作,之后进行前缀和得到结果数组,加减操作的公式:

  • 二维

递推公式:

加减公式:

8. 双指针

算法模板

当然,需要具体问题具体分析

9. 位运算

  • 取n的第k位:n>>k&1

  • 返回n的最低位1:x&-x 应用:统计n中1的个数(每次减去最低位1,直至0即可)

10. 整数有序离散化

算法思想

将一个值域很大的数组快速映射为一个数组下标,步骤如下:

  1. 去重,可用STL
1
2
3
vector<int> vec;
sort(vec.begin(),vec.end());
vec.erase(unique(vec.begin(),vec.end()),vec.end());
  1. 进行二分查找

映射的数组下标可以从0或1开始,取决于题目要求

unique()函数写法(双指针)

1
2
3
4
5
6
7
8
9
10
vector<int>::iterator unique(vector<int>& a)
{
int j = 0;
for(int i = 0;i<a.size();i++)
{
if(!i || a[i] != a[i-1]) // a[i]是第一个元素或不等于前一个元素
a[j++] = a[i];
}
return a.begin() + j;
}

11. 区间合并

算法思想

将数个有交集的区间合并为1个区间,步骤如下:

  1. 将区间按左端点排序

  2. 扫描区间,检查能否合并,此时有3种情况:

情况1:全部包含,当前维护的区间不变

情况2:区间尾部超出,则更新尾部

情况3:无交集,更新当前区间

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef pair<int,int> PII;
vector<PII> segs;

void merge(vector<PII>& segs)
{
vector<PII> res;
sort(segs.begin(),segs.end());
int start = -2e9, end = -2e9; // -infinity
for(int i = 0;i<segs.size();i++)
{
if(end<segs[i].first) // situation 3
{
if(start != -2e9) res.push_back(segs[i]);
start = segs[i].first;
end = segs[i].second;
}
else
{
end = max(end,segs[i].second); //situation 2
}
}
if(start != -2e9) res.push_back({start,end});
segs = res;
}

二 数据结构

12. 链表与邻接表

动态单链表(笔试不常用,太慢)

1
2
3
4
5
6
7
typedef struct Node{
int val;
node* next;
Node(int x): val(x), next(nullptr) {}
} *LinkedList;
// initialize
LinkedList head = new Node(); // new operator will be very slow

静态单链表(不带头节点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// definition
const int N = 100010;
int head; // head
// e stores the values of each node while ne stores the next pointer
int e[N],ne[N];
int idx; // current allocated index

// initialization
void init()
{
head = -1;
idx = 0;
}
// head insertion
void headInsert(int x)
{
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}
// insertion after index k
void insert(int k,int x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}


// delete the node right after index k
void deletion(int k)
{
if(k == -1) head = ne[head]; // delete the head
else ne[k] = ne[ne[k]];
}

静态双链表(不带头尾节点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// definition
const int N = 100010;
// l: previous pointer, r: next pointer
int e[N],l[N],r[N],idx;

// initialization
void init()
{
// index 0 stands for head, 1 stands for tail, idx starts from 2
r[0] = 1;
l[1] = 0;
idx = 2;
}


// insert at right at index k
void insert(int k,int x)
{
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx;
idx++;
}

// insert at left at inde k
insert(l[k],x)


// delete the node at index k
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}

13. 栈与队列

a. 基本模拟

数组模拟栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// definition
const int N = 100010;
int stack[N];
int top;

//initialization
void init()
{
top = -1;
}

// push
void push(int x)
{
stack[++top] = x;
}

// pop
void pop()
{
--top;
}

// if the stack is empty
bool empty()
{
return top == -1;
}

// return the top of the stack
int query()
{
return stack[top];
}

(非循环)数组模拟队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const int N = 100010;
int queue[N];
int head,tail;

void init()
{
head = 0;
tail = -1;
}

void push(int x)
{
queue[++tail] = x;
}

void pop()
{
++head;
}

bool empty()
{
return head > tail;
}

int query()
{
return queue[head];
}

b. 单调栈、单调队列

单调栈

应用:求序列中每一个数左(右)边第一个比它小(大)的数, 不存在则返回-1,栈用来维护当前元素之前的所有元素

模板(以求左边第一个最小值为例)

1
2
3
4
5
6
7
8
9
for(int i = 0;i<n;i++)
{
cin>>x;
// 如果栈顶值>=当前值,那么栈顶值永远不会输出,直接弹栈
while(top && stack[top] >= x) top--;
if(top) cout<<stack[top]<<' ';
else cout<< -1;
stack[++top] = x;
}

单调队列

应用:求滑动窗口中的最值,队列用来维护窗口

模板(以求最小值为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int N = 1000010;
int a[N],q[N]; // a: data q: index monotonic queue
int k; // k: window size
// minimum in sliding window
int head = 0, tail = -1;
for(int i = 0;i<n;i++)
{
// 队头是否已经滑出窗口
if(head <= tail && q[head] < i-k+1) head++;
// 如果队尾下标所指值>=当前值,则舍弃当前队尾,将当前值作为队尾插入
while(head <= tail && a[q[tail]] >= a[i]) tail--;
q[++tail] = i;
if(i >= k-1) printf("%d ",a[q[head]]);
}

14. KMP算法

  • next 数组的含义:

即最长公共前后缀长度

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int n,m; // length of string p and string s
char p[N],s[M];
int ne[N] // next array


// generating next array
for(int i = 2,j = 0;i<=n;i++)
{
while(j && p[i] != p[j+1]) j = ne[j];
if(p[i] == p[j+1]) j++;
ne[i] = j;
}

// matching

for(int i = 1,j = 0;i<=m;i++) // avoiding right shift of next
{
while(j && s[i] != p[j+1]) j = ne[j];
if(s[i] == p[j+1]) j++;
if(j == n)
{
// match success
}
}

15 Trie (Retrieval) 树

作用:快速存储、查找字符串集合

存储形式:用一棵树存储字符串集合,公共前缀只存1次

模板(以罗马字母为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const int N = 100010;
// 第一维是节点总数,第二维是子节点,值的含义是子节点下标
// 下标为0时,既是根节点,又空节点
int son[N][26];
// 以当前节点结尾的单词数量
int cnt[N];
// 当前下标
int idx;

// insert
void insert(char str[])
{
int p = 0;
for(int i = 0; str[i];i++)
{
int u = str[i] - 'a';
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++;
}


// query
int query(char str[])
{
int p = 0;
for(int i = 0;str[i];i++)
{
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}

16 并查集

作用:快速合并集合,快速查询两元素是否在同集合中。复杂度接近O(1)

存储形式:每个集合用一棵树存储,树根的编号就是集合编号

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const int N = 100010;

// 存储父节点
int p[N];

// 维护集合中元素数量(仅根数据有效)
int cnt[N];

void init(int n)
{
for(int i = 0;i<n;i++) p[i] = i; // all sets are seperated
}


// returns the number of the set that x lies in with path compression
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]) // 一举两得
return p[x];
}

// union
void uni(int a,int b)
{
// number of elements in a set
if(find(a) != find(b))
{
cnt[find(b)] += cnt[find(a)];
}

p[find(a)] = find(b); // b root becomes the parent of a root
}

17. 堆

模板(以小根堆为例)

支持一下操作:

  1. 初始化

  2. 插入一个数

  3. 求集合中的最小值

  4. 删除最小值

  5. 删除任意一个元素

  6. 修改任意一个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void down(int idx) // 下调
{
int min_idx = idx;
if(idx*2<=index && heap[idx*2]<heap[min_idx])
{
min_idx = idx * 2;
}
if(idx*2+1<=index && heap[idx*2+1]<heap[min_idx])
{
min_idx = idx * 2 + 1;
}
if(min_idx != idx)
{
swap(heap[min_idx],heap[idx]);
down(min_idx);
}
}


void up(int idx) // 上调
{
while(idx/2 && heap[idx/2] > heap[idx])
{
swap(heap[idx],heap[idx/2]);
idx /= 2;
}
if(is_insert)
{
ph[index_ph] = idx;
++index_ph;
}
}

void init(int n)
{
for(int i = n/2; i ;i--) down(i);
}

void insert(int val)
{
heap[index] = val;
up(index);
index++;

}


void out_min()
{
printf("%d\n",heap[1]);
}

void del_min()
{
heap[1] = heap[index];
index--;
down(1);
}

void del_any(int idx)
{
heap[idx] = heap[index];
index--;
down(idx);
up(idx);
}

void modify_any(int idx,int val)
{
heap[idx] = val;
down(idx);
up(idx);
}

18. Hash 表

离散化是特殊的Hash, 其要求hash函数单调递增

  1. 冲突处理
  • 拉链法: 将数组中的每一个单元视作一个槽,即为单链表的头节点

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const int N = 100003; // 选择离2的整数次幂较远的质数,可减少冲突概率

int h[N]; // 链表头节点数组
int e[N], ne[N];
memset(h,-1,sizeof(h));

void insert(int x)
{
int k = (x % N + N) % N; // guarantees that k is not negative
e[idx] = x;
// insert in the head
ne[idx] = h[k];
h[k] = idx++;
}
bool query(int x)
{
int k = (x % N + N) % N;
int p = h[k];
for(;e[p] != x;p = ne[p])
return p == -1;
}
  • 开放寻址法(常用线性探测法)

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const int N = 200003; // 选择离2的整数次幂较远的质数,可减少冲突概率,一般开2倍
const int MAX = 0x3f3f3f3f;
int h[N]; // hash数组
memset(h,MAX,sizeof(h));

void insert(int x)
{
int k = (x % N + N) % N; // guarantees that k is not negative
while(h[k] != MAX) k++;
h[k] = x;
}
bool query(int x)
{
int k = (x % N + N) % N;
while(h[k] != x && h[k] != MAX) k++;
if(h[k] == MAX) return false;
return true;
}
  1. 字符串前缀Hash

h[] 对应一个字符串s 的前缀Hash值,h[i] 表示s[1...i] 的Hash值,h[0] = 0

h[]计算规则如下:

  1. s[1...i]视作一个p进制的数,其10进制的值d为:

  1. h[i] = d % q

通常 p=131 or 13331 q = 1 << 64可最大限度避免冲突(实际上h[]可以用unsigned long long 存储,则无需取模了 溢出相当于取模)

  1. 由此 s中的任意子串 s[L...R] 的Hash值为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef unsigned long long ULL;
const int N = 100010;
const int P = 131;
ULL h[N];
ULL p[N]; // p[i] = P^i


void init_hash(char str[],int n)
{
p[0] = 1;
for(int i = 1;i<=n;i++)
{
p[i] = p[i-1] * P;
h[i] = h[i-1] * P + str[i];
}
}

ULL get_hash(int l,int r)
{
return h[r] - h[l-1] * p[r-l+1];
}

19 C++ STL 基础

  • vector<T>

size()

empty()

clear()

front()/back() 返回首/尾元素

push_back(T x)/pop_back()在尾部增加/删除

begin()/end() 迭代器,返回指向首元素/尾部元素后一位的迭代器(supports ++ and --)

随机访问

比较(按字典序)


  • pair<T1,T2>

first 第一个元素

second 第二个元素

比较 (以first为第一关键字,second为第二关键字,按字典序)

构造方法:

1
2
make_pair(T1 a,T2 b)
pair<T1,T2> p = {T1 a, T2 b} // C++ 11

  • string

size()

empty()

clear()

+=, ==

substr(int a, int b) a是起始下标,b是子串长度,若省略b则返回到末尾的子串

c_str() 返回字符数组的起始地址


  • queue<T>

size()

empty()

push(T x)

front()/back()

pop()

清空方法:

1
q = queue<int>(); // re-allocate a queue

  • priority_queue<T, Container=vector<T>, Compare=less<T> >(基于堆实现的优先队列,默认大根堆)

size()

empty()

push(T x)

top()

pop()

小根堆构造方式:

1
priority_queue<int,vector<int>,greater<int> > q

  • stack<T>

size()/length()

empty()

push(T x)

top()

pop()


  • deque<T>

size()

empty()

clear()

`front()/back()``

push_back(T x)/pop_back()

push_front(T x)/pop_front()

begin()/end()

随机访问


  • set<T>, multi_set<T>(RB-Tree based ordered set, set doesn't allow duplicated element while multi_set does.)

size()

empty()

insert(T x)

find(T x)(returns end() when fails)

count(T x)

erase(T x)(deletes all x)

erase(set<T>::iterator it)(deletes just at the point)

lower_bound(T x)/upper_bound(T x)(returns the smallest element iterator bigger than x/ the biggest element iterator smaller than x )

begin()/end()

操作的时间复杂度绝大多数是O(log n)


  • map<K,V>, multi_map<K,V>(RB-Tree based ordered map, mapdoesn't allow duplicated key while multi_map does.)

insert(pair<K, V> p)

erase(K key)

find(K key)

随机访问(按key)

操作的时间复杂度绝大多数是O(log n)

  • unordered_{set,multi_set,map,multimap} 与上述类似,只是元素无序

不支持lower_bound()/upper_bound() 但操作的时间复杂度绝大多数是O(1)


  • bitset(用于节省空间)

构造方法:

1
bitset<N> s; // N is the number of bits in the set

支持 ~ & | ^ << >> 等按位基本操作 以及 == != 比较操作

count() 返回1的个数

all()/any()/none() 判断是否全为1 / 至少有1个1 / 全为0

set() 全置1

set(k,v) s[k] = v

reset() 全置0

flip() 全取反

flip(k) s[k] = ~s[k]

三 搜索和图论

20 树和图的存储

树=有向无环图

无向图=特殊有向图

故一切树和图的存储都是有向图的存储

  • 邻接矩阵

  • 邻接表(常用,与拉链法Hash表思想一致)

1
2
3
4
5
6
7
8
9
10
11
// adjancent table
const int N = 100010;
int h[N], e[N*2], ne[N*2];


void add(int a,int b) // add an edge a -> b
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}

21 DFS

  • 不显式涉及树和图的DFS
  1. 全排列问题(回溯):

I:整数 n

O:1~n的所有全排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int path[N]; // N is the scale of this problem
bool visited[N];
void dfs(int u,int n)
{
if(u == n)
{
for(int i = 0;i<n;i++) printf("%d ",path[i]);
return;
}
for(int i = 1;i<=n;i++)
{
if(!visited[i])
{
// backtrace
path[u] = i;
visited[i] = true; // choose
dfs(u + 1,n);
visited[i] = false; // recover
// path[u] = 0;
}
}
}


// to invoke
dfs(0,n);
  1. n皇后问题(回溯 + 剪枝)

I:整数 n

O:n皇后问题的所有解

  1. 搜索方式1:枚举每一行,按全排列思路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
char board[N][N]; // N is the scale of this problem
bool col[N], diag[N],u_diag[N]; // 本列,对角线和副对角线


void dfs(int u,int n)
{
if(u == n)
{
for(int i = 0;i<n;i++)
{
printf("%s\n", g[i]);
}
printf("\n")
return;
}
for(int i = 0;i<n;i++)
{
if(!col[i] && !diag[u + i] && !u_diag[n - u + i]) // pruning
{
// backtrace
g[u][i] = 'Q';
col[i] = diag[u + i] = u_diag[n - u + i] = true;
dfs(u+1,n);
col[i] = diag[u + i] = u_diag[n - u + i] = false;
g[u][i] = '.';
}
}
}

// to invoke
dfs(0, n)
  1. 搜索方式2:枚举每一个格子,搜索树为二叉树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void dfs(int r,int c,int num,int n)
{
if(c == n)
{
c = 0,r++;
}
if(r == n)
{
if(num == n)
{
for(int i = 0;i<n;i++) printf("%s\n",board[i]);
printf("\n");
}
return;
}
// not putting queen in this grid
dfs(r,c+1,num,n);
// put a queen in this grid
if(!row[r] && !col[c] && !diag[r+c] && !u_diag[r-c+n]) // pruning
{
// backtrace
board[r][c] = 'Q';
row[r] = col[c] = diag[r+c] = u_diag[r-c+n]=true;
dfs(r,c+1,num+1,n);
row[r] = col[c] = diag[r+c] = u_diag[r-c+n]=false;
board[r][c] = '.';
}
}

// to invoke
dfs(0, 0, 0, n);
  • 显式涉及树或图的DFS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void dfs(int u)
{
visit[u] = true;
for(int x = h[u]; x != -1; x = ne[x])
{
int j = e[x];
if(!visit[j])
{
dfs(j);
// do something...

}
}
}

22 BFS

  • 不显式涉及树和图的BFS
  1. 最短路问题

I:一个迷宫graph

O:从左上到右下最短路径长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typedef pair<int,int> PII;

const int N = 110;

int graph[N][N], dist[N][N];
bool visit[N][N];
PII q[N*N];


int bfs(int n,int m)
{
int front = 0,tail = -1;
int dr[4] = {0,1,0,-1};
int dc[4] = {1,0,-1,0};
visit[0][0] = true;
dist[0][0] = 0;
q[++tail] = make_pair(0,0);
while(front <= tail)
{
for(int i = 0;i < 4;i++)
{
int r = q[front].first;
int c = q[front].second;
int nr = r + dr[i];
int nc = c + dc[i];
if(nr>=0 && nc >=0 && nr < n && nc < m && !visit[nr][nc] && !graph[nr][nc] )
{
dist[nr][nc] = dist[r][c] + 1;
visit[nr][nc] = true;
q[++tail] = make_pair(nr,nc);
}
}
front++;
}
return dist[n-1][m-1];

}
  • 显式涉及树或图的BFS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void bfs()
{
memset(d,-1, sizeof(d));
int front = 0,tail = -1;
q[++tail] = 1;
d[1] = 0;
while(front <= tail)
{
int t = q[front];
for(int x = h[t]; x != -1; x = ne[x])
{
int j = e[x];
if(d[j] == -1)
{
d[j] = d[t] + 1;
q[++tail] = j;
}
}
front++;
}

}
  • 拓扑排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool toposort(int n)
{
int front = 0,tail = -1;
for(int i = 1;i<=n;i++)
{
if(!ideg[i])
{
q[++tail] = i;
}
}
while(front<=tail)
{
int t = q[front];
for(int x = h[t];x!=-1;x=ne[x])
{
int j = e[x];
ideg[j]--; // delete the edge t -> j
if(ideg[j] == 0)
{
q[++tail] = j;
}
}
front++;

}
return tail == n-1; // all n-1 nodes are added into the queue
}
  • 八数码问题(华容道问题)

问题描述:

基本思想

由于9个数按3*3排列的状态只有9!种,故可以将每一种状态作为图的节点,然后用BFS求出起点到终点的最短路径

  1. 状态如何表示?可以用字符串,队列直接queue<string>

  2. 距离如何表示?可以用字典unordered_map<string,int>

solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
queue<int> x_idx;

int bfs(string start)
{
int offset[4] = {1,-1,3,-3};
string end = "12345678x";
queue<string> q;
unordered_map<string, int> d;
q.push(start);
d[start] = 0;
bool flag = false; // not found
while(q.size())
{
string cur = q.front();
if(cur == end)
{
flag = true;
break;
}
string t = cur;
int x = x_idx.front();
for(int i = 0;i <4;i++)
{
int nx = x + offset[i];
if(offset[i] == 1 && nx <= 8 && nx % 3 != 0) // not next line
{
swap(t[x],t[nx]);
if(d[t] == 0) // not visited
{
q.push(t);
d[t] = d[cur] + 1;
x_idx.push(nx);
}
}
else if(offset[i] == -1 && nx>=0 && nx % 3 != 2) // not previous line
{
swap(t[x],t[nx]);
if(d[t] == 0)
{
q.push(t);
d[t] = d[cur] + 1;
x_idx.push(nx);
}
}
else if (offset[i] == 3 || offset[i] == -3)
{
if(nx >= 0 && nx <= 8)
{
swap(t[x],t[nx]);
if(d[t] == 0)
{
q.push(t);
d[t] = d[cur] + 1;
x_idx.push(nx);
}
}
}

t = cur;
}
q.pop();
x_idx.pop();

}
if(d[end] == 0 && !flag)
{
return -1;
}
return d[end];
}

23 最短路

m is the number of edge while n is that of vertex

  1. 单源最短路
  • 所有边权均为正数
  1. 朴素Dijkstra

TC:

适用于稠密图,邻接矩阵存储

算法思想

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// adjancent matrix graph for dense graph
// 对于重边,保留一条最短边
// 对于自环,不用考虑
const int MAX = 0x3f3f3f3f;
const int N = 510;
int graph[N][N];
int dist[N];
bool s[N];
void add(int a,int b,int w)
{
graph[a][b] = min(graph[a][b],w);
}

int dijkstra(int n)
{
memset(dist,MAX,sizeof dist);
dist[1] = 0;
for(int i = 0;i<n;i++)
{
int t = -1;
for(int j = 1;j<=n;j++)
{
if(!s[j] && (t == -1 || dist[t] > dist[j])) t = j;
}
s[t] = true;
for(int j = 1;j<=n;j++)
{
dist[j] = min(dist[j],dist[t] + graph[t][j]);
}
}
if(dist[n] == MAX)
{
return -1;
}
return dist[n];
}
  1. 基于堆的Dijkstra

TC:

适用于稀疏图,邻接表存储

算法思想:

在i的基础上使用堆(可用C++ STL priority_queue)筛选dist中的最小值

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
typedef pair<int,int> PII;
typedef priority_queue<PII,vector<PII>,greater<PII> > LRH;

const int N = 150010;
const int MAX = 0x3f3f3f3f;
int h[N],ne[N],e[N],w[N],idx;
int dist[N];
bool s[N];

// 重边,自环在本题中无所谓
void add(int a,int b,int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}

int dijkstra_heap(int n)
{
memset(dist,MAX,sizeof dist);
dist[1] = 0;
LRH heap;
heap.push({0,1}); // {dist, index}
while(heap.size())
{
PII t = heap.top();
heap.pop();
int v = t.second, d = t.first;
if(!s[v])
{
s[v] = true;
for(int i = h[v]; i != -1; i = ne[i]) // i is the index of edge
{
int j = e[i]; // j is the finish point of edge i
if(dist[j] > d + w[i]) // d <-> dist[t] w[i] <-> graph[t][j]
{
dist[j] = d + w[i];
heap.push({dist[j],j});
}
}
}
}


if(dist[n] == MAX) return -1;
return dist[n];
}
  • 存在负权边
  1. Bellman-Ford

TC:

存储方式不限,仅需存储所有边,适用于有边数限制的最短路

算法思想

(若有负环——即环中各边的权值和为负——存在,则不一定存在最短路)

外层迭代的含义:

迭代k次,意味着从起点经过最多k条边到达各点的最短距离

推论:

若第n次仍有更新 -> 存在边数为n的最短路-> 最短路中存在环且权值和为负 -> 图中存在负环

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const int N = 510;
const int M = 10010;
int dist[N],backup[N];
struct Edge
{
int a,b,w;
} edges[M];


int BF(int k,int m,int n)
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
for(int i = 0;i<k;i++)
{
memcpy(backup,dist,sizeof dist); // avoid cascade updates that violates the number limit
for(int i = 0;i<m;i++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
dist[b] = min(dist[b],backup[a] + w); // relaxation
}

}
return dist[n];
}
// negative weight edges make it possible
// for unreachable distances not equal to MAX
if(dist[n] > 0x3f3f3f3f / 2) // impossible
  1. SPFA(带队列优化的BF算法)

存储方式不限,仅需存储所有边,适用于无负环的最短路

TC:

worst TC:

算法思想

在i的基础上,不再无脑更新所有dist[b],当且仅当dist[a]更新后,才更新dist[b]

此处使用BFS思想进行优化

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const int N = 100010;
const int MAX = 0x3f3f3f3f;
int h[N],ne[N],e[N],w[N],idx; // adjancent table
int dist[N];
bool s[N]; // 存储节点 i 是否在队列中


int spfa(int n)
{
memset(dist,MAX,sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
s[1] = true;
while(q.size())
{
int t = q.front();
q.pop();
s[t] = false;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if(!s[j]) // update j and j is not in the queue
{
s[j] = true;
q.push(j);
}
}
}
}

return dist[n];
}

SPFA 还可以用于判断负环,其代码只是做了一些小改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const int N = 100010;
int h[N],ne[N],e[N],w[N],idx; // adjancent table
int dist[N];
int cnt[N]; // cnt[i]: the num of edges of the SP from 1 to i
bool s[N]; // 存储节点 i 是否在队列中


bool spfa_judge(int n)
{
memset(dist,MAX,sizeof dist);
dist[1] = 0;
queue<int> q;
for(int i = 1;i<=n;i++)
{
s[i] = true;
q.push(i);
}
while(q.size())
{
int t = q.front();
q.pop();
s[t] = false;
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j]>=n)
{
return true;
}
if(!s[j]) // update j and j is not in the queue
{
s[j] = true;
q.push(j);
}
}
}
}
return false;
}
  1. 多源汇最短路
  • Floyd

TC:

算法思想

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const int N = 210;
const int MAX = 0x3f3f3f3f;

int dist[N][N]; // dist is both graph and distance

void floyd(int n)
{
for(int k = 1;k<=n;k++)
{
for(int i = 1;i<=n;i++)
{
for(int j = 1;j<=n;j++)
{
dist[i][j] = min(dist[i][j],dist[i][k]+dist[k][j]);
}
}
}
}

24 MST

  1. Prim 算法
  • 朴素 Prim(常用)

TC:

适用于稠密图

算法思想:

其中,到集合的距离定义是:

集合外的点连向集合内部点距离的最小值

算法模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int prim(int n) // returns the sum of weights in the MST
{
memset(dist,0x3f,sizeof dist);
int res = 0;
for(int i = 0;i<n;i++)
{
int t = -1;
for(int j = 1;j<=n;j++)
{
if(!st[j] && (t == -1 || dist[t] > dist[j])) t=j;
}
if(i && dist[t] == MAX) return MAX; // graph not connected

if(i) res += dist[t];
st[t] = true;

for(int j = 1;j<=n;j++)
{
dist[j] = min(dist[j],graph[t][j]); // not to the start point
}
}
return res;
}
  • 基于堆的Prim(不常用)

TC:

适用于稀疏图

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
typedef pair<int,int> PII;
typedef priority_queue<PII,vector<PII>,greater<PII> > LRH;

const int N = 150010;
const int MAX = 0x3f3f3f3f;
int h[N],ne[N],e[N],w[N],idx;
int dist[N];
bool s[N];

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

int prim_heap(int n) // returns the sum of weights in the MST
{
memset(dist,MAX,sizeof dist);
LRH heap;
heap.push({MAX,1}); // {dist, index}
int res = 0, i = 0;
while(heap.size())
{
PII t = heap.top();
heap.pop();
int v = t.second, d = t.first;
if(i && d == MAX) return MAX; // graph not connected
if(i) res += d;
if(!s[v])
{
s[v] = true;
for(int i = h[v]; i != -1; i = ne[i]) // i is the index of edge
{
int j = e[i]; // j is the finish point of edge i
if(dist[j] > w[i]) // d <-> dist[t] w[i] <-> graph[t][j]
{
dist[j] = d + w[i];
heap.push({dist[j],j});
}
}
}
++i;
}
return res;
}
  1. Kruskal 算法(常用)

TC:

适用于稀疏图

算法思想

其中第2步可以用并查集实现

算法模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const int N = 100010;
const int MAX = 0x3f3f3f3f;

int p[N]; // used in UF set to store the parent

struct Edge
{
int a,b,w;
bool operator<(const Edge& e) const
{
return w < e.w;
} // for sorting the edges by weight
}edge[2*N];

int find(int x)
{
if(x != p[x]) p[x] = find(p[x]);
return p[x];
} // UF set find ancestor

int kruskal(int n,int m) // returns the sum of weights in the MST
{
sort(edge,edge + m);
int res = 0,cnt = 0;
for(int i = 1;i <= n;i++) p[i] = i;
for(int i = 0;i < m;i++)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
a = find(a),b = find(b);
if(a!=b)
{
res += w;
cnt++;
p[a] = b;
}
}
if(cnt != n-1) return MAX;
return res;
}

25 二分图

若图中不含有奇数环(环中节点个数是奇数),则此图为二分图,反之亦然

  1. 染色法判断二分图

TC:

算法思想

基于DFS

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const int N = 100010;
const int M = 2*N;
int h[N],e[M],ne[M],idx;
int color[N]; // 染色
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}

bool dfs(int u,int c)
{
color[u] = c;
for(int i = h[u];i!=-1;i = ne[i])
{
int j = e[i];
if(!color[j]) // not colored
{
if(!dfs(j,3 - c)) return false; // color a different one
}
else if(color[j] == c) return false; // not a bi-partition graph
}
return true;
}

bool judge_bipart(int n)
{
bool flag = true;
for(int i = 1;i<=n;i++)
{
if(!color[i])
{
if(!dfs(i,1))
{
flag = false;
break;
}
}
}
return flag;
}
  1. 匈牙利算法求二分图的最大匹配

TC: 运行时间一般远小于此

算法思想

匹配:指图中一组顶点到另一组的顶点是双射

最大匹配:所含边数最多的匹配

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const int N = 510;
const int M = 100010;
int h[N],e[M],ne[M],idx;
int match[N];
bool st[N];

void add(int a,int b,int n) // n is the size of the left half of the graph
{
e[idx] = b + n; // this is important as the index is shared
ne[idx] = h[a];
h[a] = idx++;
}

bool find(int x)
{
for(int i = h[x]; i != -1;i = ne[i])
{
int j = e[i];
if(!st[j])
{
st[j] = true;
if(match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}



int hungary(int n)
{
int res = 0;
for(int i = 1;i<=n;i++)
{
// guarantees the vertex on left has chances to match
// a vertex on the right that already matched in previous
// loops
memset(st,false,sizeof st);
if(find(i)) res++;
}
return res;
}

四 数学

26 数论

质数判定

  1. 试除法

算法思想:

质数的定义

由于因数是成对出现的,故可以仅枚举n 之前的整数

TC:

算法模板:

1
2
3
4
5
6
7
8
9
10
11
12
bool is_prime(int n)
{
if(n == 1)
{
return false;
}
for(int i = 2; i <= n / i; i++) // i<=sqrt(x)
{
if(n % i == 0) return false;
}
return true;
}
  1. 埃氏筛法

筛选中所有质数

算法思想

是质数,则 不可能是质数,则删除,最终留下的是所有质数

TC: 约为 实际为

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const int N = 1000010;
int cnt; // cnt store the number of prime
int st[N];

void prime_filter(int n)
{
for(int i = 2;i<=n;i++)
{
if(!st[i])
{
cnt++;
for(int j = i + i;j <= n;j += i) st[j] = true;
}
}
}
  1. 线性筛法

筛法需要保证是被最小质因子筛掉的

TC:

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int N = 1000010;
int cnt; // cnt store the number of prime
int st[N],primes[N];

void prime_filter(int n)
{
for(int i = 2;i<=n;i++)
{
if(!st[i])
{
cnt++;
primes[cnt] = i;
for(int j = 0;primes[j]<=n/i;j++)
{
st[primes[j] * i] = true;
if(i % primes[j] == 0) break;
}
}
}
}

算法思想:

分解质因数

算法思想

基本思想同试除法

但是可能存在大于 的质因数,最后仅需特殊处理一下即可

TC:

worst:

best:

算法模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void divide(int n)
{
for(int i = 2;i<=n/i;i++)
{
if(n % i == 0)
{
int p = 0;
while(n % i == 0)
{
p++;
n /= i;
}
printf("%d %d\n",i,p); // factors and powers
}
}
if(n>1) printf("%d 1\n",n); // the factor that >sqrt(n)
printf("\n");
}

约数

  1. 试除法求所有约数:

TC:

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
vector<int> f;

void factors(int n)
{
for(int i = 1;i<=n/i;i++)
{
if(n % i == 0)
{
f.push_back(i);
}
}
int s = f.size();
for(int i = s-1;i>=0;i--)
{
int fac = n/f[i];
if(fac!=f[i]) f.push_back(fac);
}
for(int i = 0;i<f.size();i++)
{
printf("%d ",f[i]);
}
printf("\n");
f.clear();
}
  1. 求多个数乘积约数的个数与和

若一个数可表示为:

其中是质数

则其约数个数为:

其约数之和为:

约数个数算法模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const int MOD = 1e9+7;
unordered_map<int,int> power;

void divide(int x)
{
for(int i = 2;i<=x/i;i++)
{
while(x % i == 0)
{
x/=i;
power[i]++;
}
}
if(x>1) power[x]++;
}

long long res = 1;
for(auto p: power)
{
res = res * (p.second + 1) % MOD;
}

约数之和算法模板:

1
2
3
4
5
6
7
8
9
long long res = 1;
for(auto prime: power)
{
int p = prime.first;
int a = prime.second;
long long t = 1;
while(a--) t = (t * p + 1) % MOD; // generate the sum value
res = res * t % MOD;
}

最大公约数

  1. 辗转相除法

原理:

gcd(a,b) = gcd(b,a % b)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int gcd(int a,int b) // recursive
{
return b ? gcd(b,a%b) : a;
}

int gcd(int a,int b) // non-recursive
{
while(b!=0 && b!=1)
{
int temp = a;
a = b;
b = temp % b;
}
if(b == 0) return a;
else return b;
}

欧拉函数

函数定义

其中

是质数

此函数表示 中与互质的数的个数

证明过程:使用容斥原理

  1. 减去所有 的倍数

  2. 加上所有的倍数

  3. 减去所有的倍数

  4. ...直到

展开后与证明过程中得到的结果相同

朴素算法模板

同分解质因数,再用定义求即可

筛法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const int N = 1000010;
int cnt; // cnt store the number of prime
int st[N],primes[N];
int phi[N];
long long get_euler(int n)
{
phi[1] = 1;
for(int i = 2;i<=n;i++)
{
if(!st[i])
{
primes[cnt++] = i;
phi[i] = i - 1; // i is a prime number
}
for(int j = 0; primes[j] <= n / i;j++)
{
st[primes[j] * i] = true;
if(i % primes[j] == 0)
{
phi[primes[j] * i] = primes[j] * phi[i]; // pj included
break;
}
phi[primes[j] * i] = (primes[j] - 1)*phi[i]; // pj not included
}
}
long long res = 0;
for(int i = 1;i<=n;i++)
{
res += phi[i];
}
return res;
}

欧拉定理

互质,则有

证明图示: 同余

故两组数乘积模同余

同时约去

即得结论

推论:费马定理

为质数,则

快速幂

的时间复杂度中求出

算法原理:

  1. 算出来

  2. 根据 的二进制表示查表得出结果

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
int quick_power(int a,int b,int p)
{
long long res = 1;
while(b)
{
if(b & 1) res = (long long)res * a % p; // last bit of b is 1
b >>=1;
a = (long long) a * a % p; // record the power of each bit

}
return res;
}

扩展欧几里得算法

裴蜀定理

对于任意整数

证明:

那么 是整数

得证

此算法寻找 使得

算法思想

在欧几里得算法的基础上

  1. 否则不变,,这是因为在递归后:

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
int exgcd(int a,int b,int& x,int& y)
{
if(!b) // if b is zero then gcd(a,b)=a
{
x = 1,y = 0;
return a;
}
// ii
int d = exgcd(b,a%b,y,x);
y -= (a/b)*x;
return d;
}

此题解不唯一

设上述算法求解为

通解为

其中是整数

应用:线性同余方程

给定 求整数 s.t.

此问题相当于求一组 s.t.

则相当于求s.t.

其有解的充要条件为

解法是:

  1. 使用扩展欧几里得算法求 s.t.

中国剩余定理

给定两两互质的 以及

则线性同余方程组:

的解为

其中

扩展中国剩余定理

不一定两两互质

解法:合并方程

3->4: exgcd()

4->5 合并方程:

是前一个方程的解

是前一个方程的模数与当前方程模数 的最小公倍数 以下两部分为选修

26. 组合数学

27. 博弈论

五 动态规划

分析方法

当面对一个待处理的实体,会有一个操作集合,将前一种状态转换的所有情况按操作划分为不相交的子集,再从各子集中选取符合条件目标属性最值

28 背包问题

0-1背包

实体:物品

条件:每件物品最多选1次;不可超过背包容量

目标属性最值:背包中物品最大总价值

操作集合:{选,不选}

子集:{包含当前物品,不包含当前物品}

算法思想:

算法模板:

朴素实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef pair<int,int> PII;
const int N = 1010;
int dp[N][N]; // dp[i][j] max value with i items and max volume is j
vector<PII> info;


int backpack(int n, int v)
{
for(int i = 1;i <= n;i++)
{
auto d = info[i-1];
for(int j = 1;j <= v; j++)
{
if (j-d.first>=0)
{
                
dp[i][j] = max(dp[i-1][j],dp[i-1][j-d.first] + d.second);
}
else
{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][v];
}

优化为1维dp:

1
2
3
4
5
6
7
8
9
10
11
12
13
int backpack_1d(int n, int v)
{
for(int i = 1;i <= n;i++)
{
auto d = info[i-1];
for(int j = v;j >= d.first; j--)
{
// ensure that dp[i-1][j] is not modified
dp[j] = max(dp[j],dp[j-d.first]+d.second);
}
}
return dp[v];
}

完全背包

实体:物品

条件:每件物品选择次数不限;不可超过背包容量

目标属性最值:背包中物品最大总价值

操作集合:{选0个,选1个,选2个 , ...,选k个,...}

子集:{不包含当前物品,包含1个当前物品,包含2个 ,...,包含k个,...}

算法思想:

此时 i不是物品数量,而是状态编号

算法模板

朴素实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int backpack_complete(int n, int v)
{
for(int i = 1;i <= n;i++)
{
auto d = info[i-1];
for(int j = 1;j <= v; j++)
{
for(int k = 0;k * d.first <= j;k++)
{
// dp[i][j]: more than 1 may be chosen, internal comparsion
dp[i][j] = max(dp[i][j],dp[i-1][j-k*d.first]+k*d.second);
}
}
}
return dp[n][v];
}

时间复杂度优化:

思路:

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int backpack_complete(int n, int v)
{
for(int i = 1;i <= n;i++)
{
auto d = info[i-1];
for(int j = 1;j <= v; j++)
{
dp[i][j] = dp[i-1][j];
if(j>=d.first)
{
dp[i][j] = max(dp[i][j],dp[i][j-d.first]+d.second);
}
}
}
return dp[n][v];
}

1维dp优化

1
2
3
4
5
6
7
8
9
10
11
12
13
int backpack_complete_1d(int n, int v)
{
for(int i = 1;i <= n;i++)
{
auto d = info[i-1];
for(int j = d.first;j <= v; j++)
{
// doesn't have the problem in 0-1 backpack
dp[j] = max(dp[j],dp[j-d.first]+d.second);
}
}
return dp[v];
}

多重背包

实体:物品

条件:每件物品选择次数不限;物品数量有限;不可超过背包容量

目标属性最值:背包中物品最大总价值

操作集合:{选0个,选1个,选2个 , ...,选k个,...}

子集:{不包含当前物品,包含1个当前物品,包含2个 ,...,包含k个,...}

算法思想:

状态转移方程与完全背包形式相同

算法模板:

朴素实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int s[N]; // limitation of number of each item
int backpack_multi(int n, int v)
{
for(int i = 1;i <= n;i++)
{
auto d = info[i-1];
for(int j = 1;j <= v; j++)
{
for(int k = 0;k * d.first <= j && k <= s[i];k++)
{
// dp[i][j]: more than 1 may be chosen, internal comparsion
dp[i][j] = max(dp[i][j],dp[i-1][j-k*d.first]+k*d.second);
}
}
}
return dp[n][v];
}

时间复杂度优化

思路

由于max无法做减法,故无法直接用完全背包的思路进行优化

可以利用二进制思路进行优化

s[i]拆分为组,将每组视为一个整体进行0-1背包问题枚举,就可以把最内层循环的复杂度降低到

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void segmentation(int a,int b,int s)
{
int k = 1;
while(s >= k)
{
info.push_back({a*k,b*k});
s-=k;
k<<=1;
}
if(s > 0)
{
info.push_back({a*s,b*s});
}
}

int backpack_multi_log(int v)
{
for(int i = 1;i <= info.size();i++)
{
auto d = info[i-1];
for(int j = v;j >= d.first; j--)
{
// ensure that dp[i-1][j] is not modified
dp[j] = max(dp[j],dp[j-d.first]+d.second);
}
}
return dp[v];
}

分组背包

实体:物品

条件:每组物品最多选择1次;不可超过背包容量

目标属性最值:背包中物品最大总价值

操作集合:{不选,选第1个,选第2个,选第3个 , ...,选第k个,...}

子集:{不包含当前组物品,包含当前组第1个,包含当前组第2个,...,包含第k个,...}

算法思想:

类似01背包,只是体积和价值存储变为2维数组

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int N = 110;
int v[N][N], w[N][N], s[N],dp[N];

int backpack_1d(int n, int vol)
{
for(int i = 1;i <= n;i++)
{
for(int j = vol;j >= 0; j--)
{
for(int k = 0;k <= s[i];k++)
{
if(v[i][k] <= j)
{
dp[j] = max(dp[j],dp[j-v[i][k]]+w[i][k]);
}
}
}
}
return dp[vol];
}

29 线性DP

数字三角形

实体:数字

条件:只能向左或向右走

目标属性最值:路径上数组之和最大值

操作集合:{向左,向右}

子集:{所有到当前点路径}

算法模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int number_triangle(int n)
{
dp[1][1] = nums[1][1];
for(int i = 2;i<=n;i++)
{
for(int j = 1;j <= i;j++)
{
if(j - 1 >= 1 && j <= i - 1)
{
dp[i][j] = max(dp[i-1][j-1]+nums[i][j],dp[i-1][j]+nums[i][j]);
}
if (j > i-1) dp[i][j] = dp[i-1][j-1]+nums[i][j];
if(j-1 < 1) dp[i][j] = dp[i-1][1]+nums[i][j];

}
}
int ret = dp[n][1];
for(int i = 2;i<=n;i++)
{
if(dp[n][i]>ret) ret = dp[n][i];
}
return ret;
}

最长上升子序列

实体:子序列

条件:子序列严格递增

目标属性最值:子序列长度最大值

操作集合:{遍历序列中前一个数的可能编号}

子集:{以当前数结尾的上升子序列}

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int max_increasing_subsequence(int n)
{
int m = nums[1];
for(int i = 1; i<=n;i++)
{
dp[i] = 1;
for(int j = 1;j <= i-1;j++)
{
if(nums[j] < nums[i])
{
dp[i] = max(dp[j] + 1,dp[i]);
}
}
}
int res = dp[1];
for(int i = 2;i<=n;i++)
{
if(dp[i] > res) res = dp[i];
}
return res;
}

优化

思路:

实体:子序列

条件:子序列严格递增

目标属性最值:子序列长度最大值

操作集合:{寻找最大的小于当前数的上升子序列结尾}(二分)

子集:{每个上升子序列结尾的最小值}

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const int N = 100010;
// dp[i] stores the smallest number at the end of subsequence iterated to i
int nums[N], dp[N];
int max_increasing_subsequence(int n)
{
int len = 0; // max length of increasing subsequence
for(int i = 0;i < n;i++)
{
int l = 0, r = len;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(dp[mid] < nums[i]) l = mid;
else r = mid - 1;
}
len = max(len, r + 1); // len = max(len,l + 1);
dp[r + 1] = nums[i]; // dp[l + 1] = nums[i];

}
return len;
}

最长公共子序列

实体:子序列

条件:子序列是公共的

目标属性最值:子序列长度最大值

操作集合:{a[i],b[j]是否包含在子序列当中,共4种情况,见下图}

子集:{以当前长度[i,j]的两个字符串的公共子序列}

算法思想

情况1一般不写(最小)

情况2,3不完全等同于f[i-1,j]f[i,j-1]:

前者要求b[j]a[i]作为最后一个字符出现在子序列中;后者只要序列中出现b[j]a[i]即可

前者为后者子集,故后者的最大值可以代替前者最大值

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const int N = 1010;
int dp[N][N];
char a[N],b[N];
int max_common_subsequence(int n,int m)
{

for(int i = 1;i <= n;i++)
{
for(int j = 1;j <= m;j++)
{
dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]); // situation 2 and 3
if(a[i] == b[j])
{
// situation 4 only exists if a[i] == b[j]
dp[i][j] = max(dp[i][j],dp[i - 1][j - 1] + 1);
}
}
}
return dp[n][m];
}

编辑距离系列

编辑距离

最短编辑距离

实体:字符串

条件:字符串a仅经插入,删除,替换操作变为b

目标属性最值:编辑距离最小值

操作集合:{插入,删除,替换}

子集:{所有将a[1...i]变为b[1...j]的操作方式}

算法思想

  • a[1...i]删除可与b[1...j]匹配,则a[1...i-1]必与b[1...j]匹配,dp[i][j] = dp[i-1][j] + 1

  • a[1...i]插入可与b[1...j]匹配,则a[1...i]必与b[1...j-1]匹配,dp[i][j] = dp[i][j-1] + 1

  • a[1...i]替换可与b[1...j]匹配,则a[1...i-1]必与b[1...j-1]匹配,

    • a[i] == b[j] dp[i][j] = dp[i-1][j-1]

    • 否则dp[i][j] = dp[i-1][j-1] + 1

最终dp[i][j] = min(上述3种情况)

算法模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int shortest_edit_distance(int n,int m)
{
for(int i = 0; i<=m;i++) dp[0][i] = i; // insert only if len(a) is 0;
for(int j = 0;j<=n;j++) dp[j][0] = j; // delete only if len(b) is 0;
for(int i = 1;i<=n;i++)
{
for(int j = 1;j<=m;j++)
{
int del = dp[i-1][j] + 1;
int ins = dp[i][j-1] + 1;
dp[i][j] = min(del,ins);
int alt;
if(a[i] == b[j]) alt = dp[i-1][j-1];
else alt = dp[i-1][j-1] + 1;
dp[i][j] = min(dp[i][j], alt);
}
}
return dp[n][m];
}

30. 区间DP

石子合并

实体:石子

条件:仅相邻石子可以合并(不是Huffman树)

目标属性最值:合并代价最小

操作集合:{合并前1个,后n-1个;合并前2个,后n-2个;... 前k个,后n-k个;...}

子集:{所有将第i堆石子和第j堆石子合并的方式}

算法思想

基于分治思想

s[]存储前缀和

算法模板

枚举区间长度构成第1维循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int merge(int n)
{
for(int len = 2;len <= n;len++) // enumerate the length of every interval
{
for(int i = 1;i + len - 1 <= n;i++)
{
int l = i,r = i + len - 1;
dp[l][r] = 1e8; // cannot memset before the loop
for(int k = l;k < r;k++)
{
dp[l][r] = min(dp[l][r],dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
}
}
}
return dp[1][n];
}

31. 计数DP

上楼梯

整数划分

思路:

类比完全背包,此题的属性并非价值的最大值而是选法数量之和,即不是取max,而是求和

实体:物品

条件:物品总重恰好等于j

目标属性:选法的数量

操作集合:{选0个,选1个,选2个 , ...,选k个,...}

子集:{不包含当前物品,包含1个当前物品,包含2个 ,...,包含k个,...}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef long long ll;
const int MOD = 1e9 + 7;
const int N = 1010;

ll dp[N];

ll integer_partition(int n)
{
dp[0] = 1;
for(int i = 1;i<=n;i++)
{
for(int j = i; j <= n;j++)
{
dp[j] += dp[j - i] % MOD;
}
}
return dp[n] % MOD;
}

计数问题

实体:数位

条件:每个数字都要计数

目标属性:各数字出现的总次数

操作集合:{下图中的各种情况.}

子集:{在不同位上取当前数字的所有情况}

算法思想:分类讨论

边界情况

  • x 在最高位时,情况(1)不存在

  • x0时,情况(1)要从00...01开始取

C++解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int get(vector<int> digits,int r,int l)
{
int res = 0;
for(int i = r;i>=l;i--)
{
res = res * 10 + digits[i];
}
return res;
}

int pow10(int x)
{
int res = 1;
while(x--) res *= 10;
return res;
}

int count(int n,int x) // 1~n number of x appearence
{
if(!n) return 0;
vector<int> digits;
while(n)
{
digits.push_back(n % 10);
n /= 10;
}
n = digits.size();
int res = 0;
for(int i = n - 1 - !x;i >= 0;i--) // 0 will not appear at the highest position
{
if(i < n - 1) // boundary #1
{
res += get(digits,n - 1,i + 1) * pow10(i);
if(x == 0) res -= pow10(i); // boundary #2
}
if(digits[i] == x) res += (get(digits,i-1,0)+1);
else if(digits[i] > x) res += pow10(i);

}
return res;

}

// for every query
count(b,i) - count(a-1,i)

32. 状态压缩DP

在状态转移方程中,用一个整数的二进制表示状态,起到压缩状态维数的功能

蒙德里安的梦想

实体:小方格

条件:小方格铺满整个区域

目标属性:铺设方案总数

操作集合:{横向放置,纵向放置}

子集:所有可以进行转移的dp[i-1][k]

dp[i][j]表示在第i列横向放置的所有情况对应的所有方案。, 是区域的行数

e.g.:

此时方案总数为:f[1][18=(10010)]

算法思想:

当横向放置结束之后,纵向放置仅有1种方案,故此问题转化成求横向放置方案总数

状态转移方式:

条件

  • j & k == 0,即不能存在重叠铺设

  • j | k不存在奇数个连续0,即要满足纵向放置条件

方程

对满足上述转移条件的i,j,k,有dp[i][j] += dp[i-1][k]

C++解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const int R = 12;

long long dp[R][1<<R];
// st[i] stores if there are no odd-numbered consecutive 0's in a binary representation of i
bool st[1<<R];

void judge_no_odd(int r)
{
for(int i = 0; i < 1 << r; i++)
{
st[i] = true;
int cnt = 0; // number of consecutive 0's
for(int j = 0; j < r;j++)
{
if(i >> j & 1)
{
if(cnt & 1) st[i] = false;
cnt = 0;
}
else cnt++;
}
if(cnt & 1) st[i] = false;
}
}

long long numbers_of_partition(int r,int c)
{
dp[0][0] = 1;
for(int i = 1; i <= c;i++)
{
for(int j = 0; j < 1 << r;j++)
{
for(int k = 0;k < 1 << r; k++)
{
if((j & k) == 0 && st[j | k])
{
dp[i][j] += dp[i - 1][k];
}
}
}
}
return dp[c][0]; // a valid solution with no reach out in column c-1
}

最短Hamilton路径

实体:顶点

条件:每个顶点能且只能走1次

目标属性:最短Hamilton路径

操作:{求路径的长度}

子集:{dp[i][j]所有从0~j,路径经过所有点表示是i的所有路径}

e.g. dp[19=(10011)][2]

表示从0走到2经过0,1,4

状态转移

dp[i][j] = dp[i-{j}][k] + a[k][j], 其中i-{j} = i - 1 << j

C++解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const int N = 21;
int w[N][N];
int dp[1<<N][N];

int shortest_hamilton(int n)
{
memset(dp,0x3f,sizeof dp);
dp[1][0] = 0; // only 0 in the path
for(int i = 0;i < 1<<n;i++)
{
for(int j = 0;j < n;j++)
{
if(i>>j & 1) // j in the path of binary i
{
for(int k = 0;k<n;k++)
{
if(i - (1<<j)>>k & 1) // k in the path of binary (i-2^j)
{
dp[i][j] = min(dp[i][j],dp[i - (1 << j)][k]+w[k][j]);
}
}
}
}
}
return dp[(1<<n) - 1][n - 1]; // all the vertices visited ending at vertex n-1
}

33. 树形DP

没有上司的舞会

实体:节点

条件:每个节点不与其父节点同时出现

目标属性:选择的节点权值总和最大

操作:{选择当前节点,不选当前节点}

子集:{dp[u][0/1],以u为根的子树中选择,不选/选择u的方案}

采用dfs的思想, 使用邻接表存图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const int N = 6010;
int happy[N];
int dp[N][2];

int h[N],e[N],ne[N],idx;

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


void max_happy(int u)
{
dp[u][1] = happy[u];
for(int i = h[u]; i != -1;i = ne[i])
{
int j = e[i];
max_happy(j);
// not choosing u the children can be chosen
dp[u][0] += max(dp[j][0],dp[j][1]);
// choosing u the children cannot be chosen
dp[u][1] += dp[j][0];
}
}

34 记忆化搜索

DP的递归形式书写,代码更加简洁,更易理解

滑雪

实体:顶点

条件:只能从高向低走

目标属性:最长可行路径

操作集合:{向上走,向下走,向左走,向右走}

子集:{向上走,向下走,向左走,向右走}

C++递归解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const int N = 310;
int dp[N][N];
int h[N][N];

int n,m;
int dx[4] = {0,1,0,-1};
int dy[4] = {1,0,-1,0};
int longest_path(int i,int j)
{
if(dp[i][j] != -1) return dp[i][j];
dp[i][j] = 1;
for(int a = 0;a<4;a++)
{
int x = i+dx[a];
int y = j+dy[a];
if(x>=1 && x<=n && y>=1 && y<=m && h[x][y]<h[i][j])
{
dp[i][j] = max(dp[i][j],longest_path(x,y) + 1);
}
}
return dp[i][j];
}

六 贪心

采用“短视”策略,每次枚举只看当前情况下的最优解,最终得到局部最优解,在可以使用贪心策略的情况下,该局部最优解可以证明为全局最优解

35 区间问题

区间选点

思路

证明

ans是该问题最优解,cnt是按上面思路求出的解,只需证明ans<=cntans>=cnt即可

ans<=cnt是显而易见的,而根据上面的思路,我们将区间合并为cnt个不相交的区间,故至少要选cnt个点才能满足要求,即ans>=cnt。故ans=cnt

C++解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const int N = 100010;

typedef pair<int,int> PII;
vector<PII> intervals;
bool st[N];

bool cmp(const PII& a,const PII& b)
{
return a.second < b.second;
}

int min_point()
{
int res = 0;
sort(intervals.begin(),intervals.end(),cmp);
for(int i = 0;i<intervals.size();i++)
{
if(!st[i])
{
st[i] = true;
res++;
int j = i + 1;
while(j<intervals.size() && intervals[j].first<=intervals[i].second)
{
st[j] = true;
j++;
} // merge intersected intervals
}
}
return res;

}

最大不相交区间数量

思路

同上题

证明

与上题对比,上题是求合并后的最小值,此题是合并前的最大值,实质上是同一个东西。

假设同上题

ans>=cnt显而易见,假设ans>cnt,按上题的思路,仅需cnt个点即可覆盖不相交的区间,与假设矛盾,故ans<=cnt,故ans=cnt

C++解法

同上题

区间分组

思路

证明

假设同上题

ans<=cnt是显而易见的

考虑开第cnt个新组时,前cnt-1组都无法放置,又因为按左端点排序,故前cnt-1个组都包含该区间的左端点,故至少分cnt组,ans>=cnt

ans=cnt

C++解法

只需判断最小的max_r是否小于当前区间左端点即可,可用小根堆动态维护最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const int N = 100010;
typedef priority_queue<int, vector<int>, greater<int> > LRH;
struct Interval
{
int l,r;
bool operator<(const Interval& other)
{
return l < other.l;
}
}interval[N];

int min_group(int n)
{
LRH heap;
sort(interval,interval + n);
for(int i = 0;i<n;i++)
{
if(heap.empty() || heap.top() >= interval[i].l)
{
heap.push(interval[i].r);
}
else
{
heap.pop();
heap.push(interval[i].r);
}
}

return heap.size();

}

区间覆盖

思路

证明

假设同上题

在有解的情况下

对于任意ans,可调整为cnt中的解,且不会增加区间数量

C++解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const int N = 100010;
struct Interval
{
int l,r;
bool operator<(const Interval& other)
{
return l < other.l;
}
}interval[N];

int min_interval(int start,int end,int n)
{
sort(interval,interval + n);
int res = 0;
for(int i = 0;i < n;i++)
{
int max_r = -2e9;
int j = i;
while(j < n && start >= interval[j].l)
{
max_r = max(max_r,interval[j].r);
j++;
}
if(max_r < start) // no solution
{
return -1;
}
res++;
start = max_r;
if(start>=end) break;
i = j - 1;
}
if(start>=end) return res;
return -1;
}

36 Huffman树

合并果子

思路

每次挑出代价最小的两节点合并,并将合并的结果加入Huffman树中重新调整。直到树中仅有1个节点为止

证明

  • 最优解中,树中代价最小的两个节点深度一定最深,且可以互为兄弟

若树中的最小节点不是最深,则此解一定不是最优解,而同一深度下交换节点不影响结果,故可以互为兄弟。

这保证了第一次合并可以选择最小的两个节点

  • 多次贪心可以得到最优解

可以通过数学归纳法证明

C++解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
priority_queue<int,vector<int>,greater<int> > heap;

int min_cost()
{
int res = 0;
while(heap.size()>=2)
{
int t1 = heap.top();
heap.pop();
int t2 = heap.top();
heap.pop();
res += (t1+t2);
heap.push(t1+t2);

}
return res;
}

37 不等式系列

排序不等式

排队打水

思路

可以推出等待时间的公式:

直观的想法是按打水时间从小到大排序,用时较短的先打水,下面证明之

证明

反证法

假设最优解不是按照上述方法得出,则一定存在相邻的两数使得,上述公式中对应两项为:

此时交换之,式中对应的两项是:

两式相减得,故交换后的解更优,与假设矛盾。

C++解法

1
2
3
4
5
6
7
8
9
10
11
12
13
const int N = 100010;

int t[N];

long long min_wait(int n)
{
sort(t,t + n);
long long res = 0;
for(int i = 0; i < n; i++)
{
res += (t[i] * (n - i - 1));
}
return res;

绝对值不等式

货仓选址

思路

可以推导出距离和的公式:

直观的想法是放到所有商店的“中间”最好,下面证明之

证明

将原式重新分组得:

而:

故:

即建在所有商店位置得中位数处,距离和最小

C++解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const int N = 100010;

int a[N];

int min_dis(int n)
{
sort(a,a + n);
int res = 0;
for(int i = 0;i < n / 2;i++)
{
res += (a[n - 1 - i]-a[i]);
}
return res;
}

38 推公式

耍杂技的牛

思路

证明

使用反证法,假设最优解不是由上述排序得到,即存在使得

则交换前后风险为:

四项全部加上得到:

由于,则可推出:

则交换前的解并非最优值,与假设矛盾

C++解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const int N = 50010;

struct Cow
{
int w,s;
bool operator<(const Cow& other)
{
return w + s < other.w + other.s;
}
}cow[N];

int s[N];
int min_max_risk(int n)
{
sort(cow,cow+n);
for(int i = 0;i<n;i++)
{
if(i>0)
{
s[i] = s[i-1] + cow[i-1].w;
}
}
int max_risk = -0x3f3f3f3f;
for(int i = 0;i<n;i++)
{
max_risk = max(max_risk,s[i]-cow[i].s);
}
return max_risk;

}