【保研复习】C++机试笔记

基于王道机试指南的保研机试基础笔记(C++版本)

文章目录

C语言math.h函数

  1. 取绝对值

double fabs(double a); 对a取绝对值

2.取整与取余
int ceil (double a); 取上整(里面可以填整数也能填小数,整数返回自己,小数向上取整)
int floor (double a); 取下整(同上)
double modf (double a, double ip); 将参数的整数部分通过指针回传, 返回小数部分,整数部分保存在ip中
double fmod (double a, double b); 返回两参数相除a/b的余数,符号与a相同。如果b为0,则结果与具体的额实现有关

  1. 三角函数

double sin (double a); a的正弦值
double cos (double a); a的余弦值
double tan (double a); a的正切值

  1. 反三角函数

double asin (double a); 结果介于[-PI/2, PI/2],a值域为[-1,1]
double acos (double a); 结果介于[0, PI],a值域为[-1,1]
double atan (double a); 反正切(主值), 结果介于[-PI/2, PI/2]
double atan2 (double b, double a); 反正切(整圆值), 结果介于[-PI, PI]

  1. 指数与对数

double exp (double a); 幂函数ea
double pow (double a, double b); ab,如果a=0且b<=0,或者a<0且b不是整型数,将产生定义域错误
double sqrt (double a); a的平方根,其中a>=0
double log (double a); 以e为底的对数,自然对数,a>0
double log10 (double a); 以10为底的对数,a>0

  1. 双曲三角函数

double sinh (double a); a的双曲正弦值
double cosh (double a); a的双曲余弦值
double tanh (double a); a的双曲正切值

  1. 标准化浮点数

double frexp (double a, int exp); 标准化浮点数, a = f * 2^exp, 已知a求f, exp ( a介于[0.5, 1] )并返回f值
double ldexp (double a, int eap); 与frexp相反, 已知a, exp求a
2exp

模拟

  1. 图形模拟

两种思路:

​ 根据题目描述确定每一行的字符、空格的通项公式,逐行输出

​ 设置一个数组并填充、修改,最后将该数组输出出来

  1. 日期模拟
// 是否是闰年
bool isLeapYear(int year){
    if(year % 400 == 0 || (year % 100 != 0 && year % 4 == 0))
        return true;
    return false;
}
// 确定每年天数
int getYearDay(int year){
    if(isLeapYear(year))
        return 366;
    else
        return 365;
}
// 确定每月天数
int getMonthDay(int year, int month){
    int month_day = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if(isLeapYear(year))
        month_day[1] = 29;
    return month_day[month - 1];
}

第3章 排序与查找

3.1 排序

1. sort函数

sort函数的第一个参数是排序数组起始地址,第二个参数是排序数组终止地址,第三个参数是自定义比较函数。如果不定义自定义比较函数,默认升序排序。

注意自定义函数的输入为两个数组元素作为参数

struct student_info{
    int id;
    int grade;
};

// 参数类型是struct student_info,不是指针
bool Compare(struct student_info begin, struct student_info end){
    if(begin.grade == end.grade)
        return begin.id < end.id;
    else
        return begin.grade < end.grade;
}
int main(){
    struct student_info student_info_lists[100];
    sort(student_info_list, student_info_lists + n, Compare);
}
2. 经典排序算法
冒泡排序

冒泡排序思路:第i轮遍历从[i, n-1]中挑出一个最小的,通过逐个交换使其浮到最左侧i处

void BubbleSort(int *array, int n){
    bool flag;
    for(int i = 0; i < n; i++){ // 第i轮遍历
        flag = false;
        for(int j = n - 1; j > i; j--){ // 第i轮遍历从[i, n-1]中挑出一个最小的通过交换使其浮到最左侧i处
            if(array[j] < array[j - 1]){ // 升序为<,降序为>
                int temp = array[j];
                array[j] = array[j - 1];
                array[j - 1] = temp;
                flag = true;
            }
        }
        if(!flag) return;
    }
}
选择排序

选择排序思路:第i轮遍历从[i, n-1]中挑出一个最小的,直接使其与i处元素交换

void selectSort(int* array, int n){
    for(int i = 0; i < n - 1; i++){
        // 从i到n-1中挑出一个最小的
        int min_pos = i;
        for(int j = i + 1; j < n; j++){
            if(array[min_pos] > array[j]){
                min_pos = j;
            }
        }
        // 交换min_pos和i的位置
        if(min_pos != i){
            int temp = array[min_pos];
            array[min_pos] = array[i];
            array[i] = temp;
        }
    }
}
插入排序

每轮排序将待排序序列的第一个元素插入已排序序列的对应位置

void insertSort(int *array, int n){
    int temp;
    for(int i = 1; i < n; i++){ // 已排序序列为array[0]
        temp = array[i]; // 待排序元素为array[i](i从下标1开始)
        int j;
        // 对于从0到i-1的已排序序列,如果待排序元素temp小于他们,将他们后移一个位置
        for(j = i - 1; j >= 0 && temp < array[j]; j--){
            array[j + 1] = array[j];
        }
        // 直到大于temp的元素全部后移完成,空出来的位置j+1存放temp
        array[j + 1] = temp;
    }
}
快速排序

分治法思想:找到一个元素,将比它小的都放在左边,比它大的都放在右边,这样就确定了该元素应该放置的位置,然后分别再对其左右执行相同操作

void swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}
void quickSort(int *array, int left, int right){
    if(left < right){
        int key = array[left];
        int i = left, j = right + 1;
        while(1){
            // 找到key左边第一个比key大的元素
            while(array[++i] < key && i != right);
            // 找到key右边第一个比key小的元素
            while(array[--j] > key && j != left);
            // 如果i < j互换位置,否则本轮排序完成
            if(i < j) swap(&array[i], &array[j]);
            else break;
        }
        // j为排序元素key该在的位置
        swap(&array[left], &array[j]);
        // 分治
        quickSort(array, left, j - 1);
        quickSort(array, j + 1, right);
    }
}
int array[5] = {5, 3, 6, 2, 8};
quickSort(array, 0, 4);
归并排序

将左右两个子序列排序完成,然后使用merge函数将其合并

void mergeSort(int *array, int n){
    int *temp = (int*)malloc(sizeof(int) * n);
    mSort(array, temp, 0, n - 1);
}
void mSort(int *array, int *temp, int left, int right){
    int middle;
    if(left < right){
        middle = left + (right - left) / 2;
        mSort(array, temp, left, middle);
        mSort(array, temp, middle + 1, right);
        merge(array, temp, left, middle, right);
    }
}
void merge(int *array, iny *temp, int left, int leftend, int rightend){
    int i = left, j = leftend + 1, k = left;
    while(i <= leftend && j <= rightend){
        if(array[i] <= array[j]){
            temp[k++] = array[i++];
        }
        else
            temp[k++] = array[j++];
    }
    while(i <= leftend)
        temp[k++] = array[i++];
    while(j <= rightend)
        temp[k++] = array[j++];
    for(int i = left; i <= right; i++)
        array[i] = temp[i];
}

3.2 查找

二分查找

二分查找的前提是数组有序

int BinarySearch(int *array, int len, int key){
    int left, right, middle;
    left = 0;
    right = len - 1;
    while (left <= right){
        middle = left + (right - left) / 2;
        if(array[middle] > key){
            right = middle - 1;
        } else if(array[middle] < key){
            left = middle + 1;
        } else{
            printf("The index is ");
            break;
        }
    }
    if(left > right) return -1;
    else return middle;
}

第4章 字符串

4.1/4.2 字符串与字符串处理

string中函数

string类型的长度

int len = str.size();

string类型的遍历

string str = "hello";
int len = str.size();
// 下标访问
for(int i = 0; i < len; i++)
    printf("%s ", str[i]);
// 迭代器访问
for(string::iterator i = str.begin(); i != str.end(); i++)
    printf("%s ", *i);

string类型的读入

string str;
// cin方式:简单,但执行较慢
cin >> str;
// scanf方式
str.resize(50); // 预先分配空间
scanf("%s", &str[0]); // 读入

string运算符

+运算符:拼接字符串

比较大小:按照字典序比较

string常用函数

  1. insert():在字符串指定位置插入某子串
string str = "hello";
// 插入元素
str.insert(2, "ppp"); // str为"hpppello"
  1. find():在字符串中寻找字符或字符串,找到返回起始下标,找不到返回string::npos
string str = "hello world";
int found = str.find("world");
if(found != string::npos)
    printf("find 'world' at %d\n", found); // 打印6
  1. substr():返回字符串的子串,第一个参数是子串起始位置,第二个参数是子串长度
string str = "hello world";
string sub_str = str.substr(6, 5); // substr为world
  1. erase():第一个参数为起始位置,第二个参数为子串长度,将这一部分子串切割掉
string str = "hello world";
str.erase(0, 4); // str变为"o world"

注意:insert、find、erase函数都是直接改变原字符串,而substr是返回得到的子串,原字符串不变

cstring中函数

只能用于char*

字符串长度

char a[] = "ABC";

int l = strlen(a); // 返回3,不包含'\0'的长度

int l = sizeof(a) / sizeof(char); // 返回4,包含'\0'的长度

字符串切割

char buf[] = "abckd$kdf%kdlfk";
char *p = strtok(buf, "%$"); // 以$或,将字符串buf切割,即遇到$或,都会切割,并将第一个子串返回给p
while(p != NULL) // strtok会一直切割,直到返回NULL,切割完成
{
    printf("%s\n", p);
    p = strtok(NULL, "%$"); // 获取第二个子串时,strtok的第一个参数写NULL即可
}

字符串复制

char src[] = "source";
char dest[10];
strcpy(dest, src);
printf("%s\n", dest); // 输出source

strncpy(dest, src, 2);
dest[2] = '\0';
printf("%s\n", dest); // 输出so

字符串拼接

strcat

char a[] = "aaa";
char b[] = "bbb";
strcat(a, b); // 将b拼接到a上
printf("%s\n", a); // 输出aaabbb

字符串比较

char b[] = "bbb";
char b2[] = "bbb";
char a[] = "aaa";
char c[] = "ccc";
printf("b cmp b2 is %d\n", strcmp(b, b2)); // b = b2返回0
printf("b cmp a is %d\n", strcmp(b, a)); // b > a 返回1
printf("b cmp c is %d\n", strcmp(b, c)); // b < c 返回-1
char a1[] = "aaabbb";
char a2[] = "aaaccc";
printf("a1 cmp a2 in first 3 * sizeof(char) is %d\n", strncmp(a1, a2, 3 * sizeof(char))); // 只比较前三个字节,返回0
printf("a1 cmp a2 in first 4 * sizeof(char) is %d\n", strncmp(a1, a2, 4 * sizeof(char))); // 比较前四个字节,返回-1

字符串查找

char* p1 = "abcdefgh";
char* p2 = "def";
char* ret = strstr(p1, p2);//把返回的字符串首地址赋给ret
if (ret == NULL){
    printf("子串不存在\n");//当返回的字符串首地址为空,ret为一个空指针,代表不存在该子串
}
else{
    printf("%s\n", ret);//当返回的字符串首地址不为空,则会从字符串首地址开始打印,到‘\0’停止
}

4.3 字符串匹配

字符串匹配:KMP算法

给出一个文本串text和模式串pattern,寻找模式串pattern在文本串text中存在的起始位置

思路

对text和pattern进行匹配时,如果pattern的前k-1位都匹配成功了,第k位匹配失败,则再次移动pattern重新匹配时,可以不从头开始,而是根据next数组的记录,跳过右移pattern后确定能够匹配的部分,提高匹配效率。

next数组的计算方式为构造法:

例如

如下表1,pattern前五位匹配成功,第6位匹配失败

text: A B C A B C D H I J K

pattern: A B C A B B

传统方式中,pattern的下次匹配应该是pattern右移一位重新匹配,如下表2

text: A B C A B C D H I J K

pattern: A B C A B B

而KMP算法中,next数组计算了表1中匹配成功的那个子串(A B C A B)的最长公共前后缀

为什么要寻找最长公共前后缀呢,因为pattern右移后,相当于将text已匹配子串的后缀与pattern已匹配子串的前缀进行匹配,如果已匹配子串的前后缀存在公共部分,即匹配成功,因此text的指针继续匹配即可,不需要回退操作,也就提高了效率

text: A B C A B C D H I J K

pattern: A B C A B B

算法

// 求next数组,即求最长匹配前后缀
int* getNextTable(string pattern) {
        int m = pattern.size();
        int *next = (int *)malloc(sizeof(int) * (m + 2));
        int i = -1; // i是最长匹配前缀的最后一个字符的下标,初始化为-1(同时也是前后缀匹配长度)
        int j = 0; // j是最长匹配后缀的最后一个字符的下标,初始化为0
        next[0] = i;
        while(j < m){
            if(i == -1 || pattern[i] == pattern[j]){
                // 如果两个前后缀的下一个字符仍然匹配,直接加一即可
                i++;
                j++;
                next[j] = i; // j处的next值为前后缀长度i
            }
            else
                i = next[i];
            // 如果下一个字符不匹配,将i回退回nextTable[i]处继续寻找前后缀
        }
        return next;
    }
// KMP算法寻找text中第一个匹配pattern的位置
int KMP1(string text, string pattern){
    int* next = getNextTable(pattern);
    int n = text.size(), m = pattern.size();
    int i = 0; // text中的指针
    int j = 0; // pattern中的指针
    while(i < n && j < m){
        if(j == -1 || text[i] == pattern[j]){
            // 如果当前字符匹配成功,继续匹配下一个字符
            i++;
            j++;
        }
        else{
            // 如果当前字符匹配不成功,pattern指针改到nextTable[j]处,i指针不用变,继续匹配text[i]的字符
            j = next[j];
        }
    }
    
    // 如果pattern指针移动到了最后,说明pattern全串匹配成功,i为匹配到的子串的末尾位置,i-j+1即为匹配到的子串的起始位置
    if(j == m)
        return i - j;
    else
        return -1;
    // 否则匹配不成功
}
// KMP算法寻找text中匹配pattern的数量
int KMP2(string text, string pattern){
    ... // 初始化操作如上省略
    int count = 0;
    while(i < n){
        if(j == -1 || text[i] == pattern[j]){
            // 如果当前字符匹配成功,继续匹配下一个字符
            i++;
            j++;
        }
        else{
            // 如果当前字符匹配不成功,pattern指针改到nextTable[j]处,i指针不用变,继续匹配text[i]的字符
            j = next[j];
        }
        if(j == m){
            count++;
            j = next[j];
        }
    }
    return count;
}

第5章 数据结构一

5.1 向量vector

头文件

#include<vector>

定义

vector<typename> name;

函数

empty():判断向量是否为空

size():返回元素个数

push_back():添加元素

pop_back():删除最后一个元素

insert():在任意位置插入元素

// insert()的三种用法
vec.insert(vec.begin() + 2, 't'); // 在vec的2位置插入元素't'
vec.insert(vec.begin() + 2, 3, 't'); // 在vec的2位置插入3个't'
vector<int> vec2;
vec2.push_back(4);
vec2.push_back(5);
vec.insert(vec.begin(), vec2.begin(),)

erase():删除任意位置的元素 vec.erase(vec.begin() + 5); 删除下标为6的元素

clear():清空向量

remove(vec.begin(), vec.end(), 4); // 删除vec中所有的4

迭代器:

vector<int> numbers;
for(vector<int>::iterator i = numbers.begin(); i != numbers.end(); i++){
    printf("%d", *i);
}

查找:

vector<int> a;
a.push_back(1);
a.push_back(2);
a.push_back(3);
if(find(a.begin(), a.end(), 1) != a.end())
    cout << "找到了" << endl;

5.2 队列queue

只能从尾部插入元素(入队),从头部取出元素(出队),特点先进先出

头文件

#include<queue>

定义

queue<typename> name;

函数

empty():判断queue是否为空

size():判断queue元素数量

push():队尾入队

pop():队首出队

front():获得队首元素

back():获得队尾元素

用队列实现循环链表解决约瑟夫问题

思路,对于不该死的人从队首pop出去后(报数后)再push进队尾(还活着等待下一轮报数),对于该死的人,不需要再push进入

int n, m, p; // n是所有人数,m是报数到m的人死亡,p是报数起始人数
queue<int> life;
// 从报数起始人p开始入队
for(int i = 0; i < n; i++)
    life.push((p + i) % n);
while(!life.empty()){
    // m-1个不死的人,pop出去后再加回队尾,以实现循环链表的功能【1】
    for(int i = 1; i < m; i++){
        life.push(life.front());
        life.pop();
    }
    // 报数报到m的人死去
    life.pop();
}

【1】处不是先pop再push是因为c++语法限制,queue先pop再push会导致编译报错

另外数学公式法

// f(n, m) = (f(n−1, m) + m) % n
int f(int n, int m){
    if(n == 1) return 0;
    return (f(n-1, m) + m) % n;
}
int lastRemaining(int n, int m){
    return f(n, m);
}

5.3 栈stack

只能栈顶进或出,栈底不能进或出,特点后进先出

头文件

#include<stack>

定义

stack<typename> name;

函数

empty()

size()

push()

pop()

top():获取栈顶元素

应用:表达式求值

思路:

设置一个数字栈,一个符号栈。遍历字符串,如果遇到数字就将其放入数字栈中,如果遇到操作符,有两种可能:

  1. 栈顶操作符比当前操作符优先级低,则当前操作符直接入栈即可
  2. 栈顶操作符比当前操作符优先级高,则应当先将栈顶操作符及数字栈的前两个数字取出进行计算,再将计算结果放回数字栈(注意此处不应将当前操作符立刻入栈,也不应进行index++的操作,因为下一个栈顶操作符的优先级有可能仍然比当前操作符优先级高,需要再进行一次本步骤操作)

需要注意的是,本程序中在操作符栈中提前push进了一个优先级最低的’#‘,并在字符串末尾加了一个优先级次低的’$',其作用是:

  1. '#‘的作用:优先级比任何操作符都低,保证符号栈中为空时下一个符号能够经过与’#'的优先级比较正常push进来
  2. ' ′ 的作用:比字符串中其他操作符都低,保证字符串中没有其他符号时仍然能够经过 与 ′ '的作用:比字符串中其他操作符都低,保证字符串中没有其他符号时仍然能够经过与' 的作用:比字符串中其他操作符都低,保证字符串中没有其他符号时仍然能够经过'的优先级比较完成符号栈中剩余符号的计算

括号的处理方法:如果遇到左括号,将其放入栈中;如果遇到右括号,将符号栈中符合push出来并计算,直到push到左括号为止,最后将括号的计算结果放入num_stack中

int getNum(string str, int *index){
    int temp = 0;
    while(isdigit(str[*index])){
        temp = temp * 10 + str[*index] - '0';
        (*index)++;
    }
    return temp;
}

bool isOp(char c){
    if(c == '+' || c == '-' || c == '*' || c == '/' || c == '$')
        return true;
    else
        return false;
}

int getPriority(char c){
    if(c == '*' || c == '/')
        return 4;
    else if(c == '+' || c == '-')
        return 3;
    else if(c == '$')
        return 2;
    else if(c == '#')
        return 1;
    return -1;
}

double calculate(double a, char op, double b){
    if(op == '+')
        return a+b;
    else if(op == '-')
        return a-b;
    else if(op == '*')
        return a*b;
    else if(op == '/')
        return a/b;
    return -1;
}

int main(){
    string str;
    while(getline(cin, str)){
        if(str == "0")
            break;
        stack<double> num_stack;
        stack<char> op_stack;
        op_stack.push('#');
        str = str + '$';
        int len = str.size();
        int index = 0;
        while(index < len){
            if(isdigit(str[index])){
                double t = (double)getNum(str, &index);
                num_stack.push(t);
            }
            else if(isOp(str[index])){
                if(getPriority(op_stack.top()) >= getPriority(str[index])){
                    double b = num_stack.top();
                    num_stack.pop();
                    double a = num_stack.top();
                    num_stack.pop();
                    char op = op_stack.top();
                    op_stack.pop();
                    double temp = calculate(a, op, b);
                    num_stack.push(temp);
                }
                else{
                    op_stack.push(str[index]);
                    index++;
                }
            }
            else if(str[index] == '('){
                op_stack.push(str[index]);
                index++;
            }
            else if(str[index] == ')'){
                while(1){
                    char op = op_stack.top();
                    op_stack.pop();
                    if(op == '('){
                        break;
                    }
                    double b = num_stack.top();
                    num_stack.pop();
                    double a = num_stack.top();
                    num_stack.pop();
                    double temp = calculate(a, op, b);
                    num_stack.push(temp);
                }
                index++;
            }
            else
                index++;
        }
        printf("%.02lf\n", num_stack.top());
    }
}

5.4 map

map<string, int> my_map;
// 插入
my_map.insert(pair<string, int>("abc", 1));
my_map["abc"] = 1;
// 查找
map<string, int>::iterator it;
it = list.find("abc");
if(it != list.end()){// 判断是否查找成功
    int value = it->second; //获取"abc"对应的结果
}

第6章 数学问题

6.1 进制转换

#include<iostream>
#include<cstdio>

using namespace std;
// m进制转十进制
long long m_into_ten(string x_m, int m){
    int x_m_len = x_m.size();
    long long ret = 0;
    for(int i = 0; i < x_m_len; i++){
        long long temp;
        if(x_m[i] >= '0' && x_m[i] <= '9')
            temp = x_m[i] - '0';
        else
            temp = x_m[i] - 'a' + 10;
        ret = ret * m + temp;
    }
    return ret;
}
// 十进制转n进制
string ten_into_n(long long x_10, int n){
    string ret = "";
    while(x_10 != 0){
        int temp = x_10 % n;
        if(temp < 10)
            ret += temp + '0';
        else
            ret += temp - 10 + 'a';
        x_10 /= n;
    }
    // reverse
    string ret2 = "";
    int len = ret.size();
    for(int i = len - 1; i >= 0; i--)
        ret2 += ret[i];
    return ret2;
}

int main(){
    int m, n;
    string x;
    scanf("%d %d", &m, &n);
    cin >> x;
    int x_len = x.size();
    long long t = m_into_ten(x, m);
    cout << ten_into_n(t, n) << endl;
    return 0;
}

6.2 最大公约数和最小公约数

最大公约数GCD

辗转相除法:利用“a与b的最大公约数也能够整除a mod b”

int GCD(int a, int b){
    if(b == 0)
        return a;
    else
        return GCD(b, a % b);
}

最小公倍数LCM

利用公式“两个数的乘积等于他们的最大公约数 * 最小公倍数”

LCM = a * b / GCD(a, b);

6.3 质数

对于数x,从2到sqrt(x),判断是否能整除

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

bool isPrime(int x){
    int end = sqrt(x);
    for(int i = 2; i <= end; i++)
        if(x % i == 0)
            return false;
    return true;
}

int main(){
    int x;
    while(scanf("%d", &x) != EOF){
        if(x > 1 && isPrime(x))
            printf("yes\n");
        else
            printf("no\n");
    }
}

素数筛法

思路:因为一个合数一定是某个质数的倍数,故每次找到一个质数,就将其所有倍数置为false,遍历一遍即可标记出所有的质数和合数

const int MAXN = 10000;
vector<int> prime;
bool prime_flag[MAXN];

void initial_flag(){
    memset(prime_flag, true, MAXN);
    prime_flag[0] = false;
    prime_flag[1] = false;
    for(int i = 2; i < MAXN; i++){
        // 之前作为质数的倍数被设置为false,说明是合数,直接跳过
        if(!prime_flag[i])
            continue;
        // 对于质数,将其放入质数队列,并将其倍数全部设置为合数
        prime.push_back(i);
        for(int j = i * i; j < MAXN; j += i)
            prime_flag[j] = false;
    }
    return;
}

第7章 贪心策略

第8章 递归与分治

第9章 搜索

BFS 广度优先搜索

如果节点A有三个子节点a、b、c,a节点有两个子节点1、2,则访问顺序为

A - a - b - c - 1 - 2

可见BFS是先访问兄弟节点,再访问子节点

BFS除了可以用于遍历之外,还可以用于寻找没有权重的最短路/最近节点等

#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>

using namespace std;

const int MAXN = 100000;
// 一个节点可以连通的其他四个节点,此处假设为可以后退两步或一步或前进两步或一步
int change[5] = {-2, -1, 1, 2};

// 存放位置与距离的结构体
struct position{
    int pos;
    int dis;
    position(int p, int d): pos(p), dis(d) {}
};
// 标记该位置是否已访问过
bool visited[MAXN];
// num为节点个数,start为广度优先遍历的起始位置
int BFS(int start, int target){
    queue<position> q;
    memset(visited, false, sizeof(visited));
    // 将起点入队,距离为0
    q.push(position(start, 0));
    // 设置起点为已访问
    visited[start] = true;
    while (!q.empty()){
        // 出队一个节点
        position temp = q.front();
        q.pop();
        // 如果是目的终点,直接返回
        if(temp.pos == target){
            return temp.dis;
        }
        // 广度优先遍历该节点的所有连通节点
        for(int i = 0; i < 4; i++){
            position new_node = position(temp.pos + change[i], temp.dis + 1);
            // 如果新节点超出范围或已访问过,直接跳过
            if(new_node.pos < 0 || new_node.pos >= MAXN || visited[new_node.pos]){
                continue;
            }
            // 将新节点入队并计为已访问
            q.push(new_node);
            visited[new_node.pos] = true;
        }
    }
}

DFS 深度优先搜索

如果节点A有三个子节点a、b、c,a节点有两个子节点1、2,则访问顺序为

A - a - 1 - 2 - b - c

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

const int MAXN = 30;

// 标记该位置是否已访问过
bool flag[MAXN][MAXN];
// 棋盘长度p、宽度q
int p, q;

bool DFS(int x, int y, int step, string ans){
    if(step == p * q){
        cout << ans << endl;
        return true;
    }
    // 遍历周围八个日字邻居节点
    int x2, y2;
    for(int i = 0; i < 8; i++){
        if(i == 0){
            x2 = x + 1;
            y2 = y + 2;
        } else if(i == 1) {
            x2 = x + 1;
            y2 = y - 2;
        } else if(i == 2) {
            x2 = x - 1;
            y2 = y + 2;
        } else if(i == 3) {
            x2 = x - 1;
            y2 = y - 2;
        } else if(i == 4) {
            x2 = x + 2;
            y2 = y + 1;
        } else if(i == 5) {
            x2 = x + 2;
            y2 = y - 1;
        } else if(i == 6) {
            x2 = x - 2;
            y2 = y + 1;
        } else if(i == 7) {
            x2 = x - 2;
            y2 = y - 1;
        }
        if(x2 < 0 || y2 < 0 || x2 >= p || y2 >= q || flag[x2][y2]){
            continue;
        }
        flag[x2][y2] = true;
        char col = x2 + 'A';
        char row = y2 + '1';
        // 如果本条路径深度搜索成功,直接返回
        if(DFS(x2, y2, step + 1, ans + col + row)){
            return true;
        }
        // 如果失败,将本路径修改回退,重新尝试邻居节点
        flag[x2][y2] = false;
    }
    return false;
}

int main(){
    scanf("%d %d", &p, &q);
    memset(flag, false, sizeof(flag));
    flag[0][0] = true;
    if(!DFS(0, 0, 1, "A1")){
        printf("impossible\n");
    }
    return 0;
}

第10章 数据结构二

10.1 二叉树

数据结构
struct treeNode{
    ElementType data;
    treeNode *left;
    treeNode *right;
    treeNode(ElementType c): data(c), left(NULL), right(NULL) {}
}
前中后序遍历
void PreOrder(treeNode *root){
    if(root == NULL)
        return;
    visit(root->data);
    PreOrder(root->left);
    PreOrder(root->right);
    return;
}

中序、后序遍历同理

层次遍历

void levelOrder(TreeNodePoint root){
    queue<TreeNodePoint> node_queue;
    TreeNodePoint temp;
    node_queue.push(root);
    while(!node_queue.empty()){
        temp = node_queue.front();
        node_queue.pop();
        visit(temp);
        if(temp->lchild != NULL)
            node_queue.push(temp->lchild);
        if(temp->rchild != NULL)
            node_queue.push(temp->rchild);
    }
}
由前序遍历字符串递归创建二叉树(#代表空树)
string str;
int pos = 0;
treeNode *buildTree(){
    char c = str[pos];
    if(c == '#'){
        return NULL;
    }
    treeNode *root = new treeNode(c);
    root->left = buildTree(); // 还原二叉树同样递归处理
    root->right = buildTree();
}
前序遍历和中序遍历能唯一确定一个二叉树

思路:前序遍历的第一个节点一定是根节点,而该根节点在中序遍历中的位置可以分别确定左右子树

// pre_str是前序遍历序列,in_str是中序遍历序列
treeNode *buildTree(string pre_str,string in_str){
    if(pre_str.size() == 0)
        return NULL;
    // 前序遍历的第一个字符是当前子树的根节点
    char root_data = pre_str[0];
    // 寻找根节点在中序遍历中的位置(同时也是左子树的长度)
    int pos = in_str.find(root_data);
    treeNode *tree = new treeNode(root_data);
    
    // 左子树的前序遍历序列为pre_str从1到pos,中序遍历序列为0到pos
    tree->left = buildTree(pre_str.substr(1, pos), in_str.substr(0, pos));
    // 右子树的前序遍历序列为pre_str从pos+1到结尾,中序遍历序列为pos + 1到结尾
    tree->right = buildTree(pre_str.substr(pos + 1), in_str.substr(pos + 1));
    return tree;
}

10. 2 二叉搜索树/排序树

二叉搜索树/排序树TODO

左子树所有节点值都小于根节点值

右子树所有节点值都大于根节点值

如果对二叉搜索树进行中序遍历,会得到一个升序序列

平衡二叉树TODO

条件:

是二叉排序树

任何一个节点的左子树和右子树都是平衡二叉树

10.3 优先队列

规则:高优先级先出

STL-priority_queue

#include<queue>

priority_queue<int> myPriorityQueue;
// priority_queue的size()、top()、pop()、push()函数与queue相同
myPriorityQueue.push(20);
myPriorityQueue.push(100);
myPrioirtyQueue.push(3);

cout << myPriorityQueue.top() << endl; // 输出100
myPriorityQueue.pop();
cout << myPriorityQueue.top() << endl; // 输出20
myPriorityQueue.pop();

复杂类型的优先队列

// 复数
struct complex{
    int real; // 实部
    int imag; // 虚部
    complex(int a, int b): real(a), imag(b) {}
    bool operator< (Complex c) const { // 重载小于号
        return real * real + imag * imag < c.real * c.real + c.imag * c.imag;
    }
};
哈夫曼树TODO

10.4 散列表

第11章 图论

BFS(可以用来判断图是否连通、连通分量的个数,还可以用于无权图的最短路径问题)

对于连通图,在BFS_graph()、DFS_graph()函数中调用一次BFS_ver()或DFS_ver()即可遍历完成,对于非连通图,调用BFS_ver()或DFS_ver()的次数即为其连通分量的个数。

#define MAXN 505
struct Edge{
    int to;
    int length;
    Edge(int t, int l): to(t), length(l){}
};
vector<Edge> graph[MAXN];
int visited[MAXN];
int visit(int x){}
void BFS_ver(int x){
    queue<int> Q;
    visit(x);
    visited[x] = 1;
    Q.push(x);
    while(!Q.empty()){
        int temp = Q.front();
        Q.pop();
        for(int i = 0; i < graph[temp].size(); i++){
            int v = graph[temp][i].to;
            visit(v);
            visited[v] = 1;
            Q.push(v);
        }
    }
}
void BFS_graph(int n){
    memset(visited, 0, sizeof(visited));
    int num = 0;
    for(int i = 0; i < n; i++){
        if(!visited[i]) {
            BFS_ver(i); // 调用BFS_ver的次数即为连通分量的个数,如果图是连通图,只需要调用BFS_ver一次
            num++; // num为连通分量的个数
        }
    }
    printf("%d\n", num);
}

DFS

struct Edge{
    int to;
    int length;
    Edge(int t, int l): to(t), length(l){}
};
vector<Edge> graph[MAXN];
int visited[MAXN];
int visit(int x){
    printf("visit %d\n", x);
}
void DFS_ver(int x){
    visit(x);
    visited[x] = 1;
    for(int i = 0; i < graph[x].size(); i++){
        int v = graph[x][i].to;
        if(!visited[v])
            DFS_ver(v);
    }
}
void DFS_graph(int n){
    memset(visited, 0, sizeof(visited));
    int num = 0;
    for(int i = 0; i < n; i++)
        if(!visited[i]){
            DFS_ver(i); // 在此处调用DFS_ver的次数即为连通分量的个数
            num++;
        }
    printf("%d\n", num);
}

单源最短路径的Dijkstra算法

求图G(V, E)中某起点到终点之间的最短路径(边权和最小的路径)

#include<iostream>
#include<cstdio>
#include<vector>
#include<cstring>
#include<climits>
#include<queue>

using namespace std;

const int MAXN = 200;
const int INF = INT_MAX;  // INT的最大值

struct Point{
    int id;        // 点的编号
    int distance;  // 源点到该点的距离
    Point(int i, int d): id(i), distance(d) {}
    bool operator< (const Point& p) const {
        return distance > p.distance;   // 距离小的优先级高
    }
};
struct Edge{
    int to;        // 终点
    int length;    // 道路长度
    Edge(int t, int l): to(t), length(l) {}
};

vector<Edge> graph[MAXN];  // 图
int dis[MAXN];

void Dijkstra(int begin);

int main(){
    int n, m;
    scanf("%d %d", &n, &m);   // n是城镇数和道路数
    memset(graph, 0, sizeof(graph));  // 初始化图
    fill(dis, dis + n + 5, INF);  // 距离初始化为无穷,注意要多初始化几个,因此有可能从1开始
    while(m--){
        int from, to, length;
        scanf("%d %d %d", &from, &to, &length);
        graph[from].push_back(Edge(to, length));
        graph[to].push_back(Edge(from, length));
    }
    int begin, end;
    scanf("%d %d", &begin, &end);
    Dijkstra(begin);
    if(dis[end] == INF){
        dis[end] = -1;   // 终点不可达,设置为-1
    }
    printf("%d\n", dis[end]);
}
void Dijkstra(int begin){
    priority_queue<Point> myPriorityQueue;
    dis[begin] = 0;
    myPriorityQueue.push(Point(begin, dis[begin]));
    while(!myPriorityQueue.empty()){
        int u = myPriorityQueue.top().id;  // 离源点最近的点
        myPriorityQueue.pop();
        for(int i = 0; i < graph[u].size(); i++){
            int v = graph[u][i].to;
            int d = graph[u][i].length;
            if(dis[v] > dis[u] + d){
                dis[v] = dis[u] + d;
                myPriorityQueue.push(Point(v, dis[v]));
            }
        }
    }
}

多源最短路径的Floyd算法

const int max_n = 505;
int dis[max_n][max_n]; // 用邻接矩阵存储

void Floyd(int n){
    for(int k = 1; k <= n; k++)// 中间结点为k
        for(int i = 1; i <= n; i++) // i、j为两边节点
            for(int j = 1; j <= n; j++) // 尝试在所有i、j直接加入k,看是否会变近
                dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}

int main(){
    int n, m;
    int u, v;
    scanf("%d %d", &n, &m);
    fill(dis, dis + max_n * max_n, INT_MAX);
    for(int i = 0; i < m; i++){
        scanf("%d %d", &u, &v);
        scanf("%d", &dis[u][v]);
    }
    Floyd(n);
    return 0;
}

并查集(用于判断图是否连通、求连通分量的个数、求添加多少条边可使图连通)

如果最后Union的树只有一个,则图连通

连通分量个数即为树的个数,即为根节点个数,即为Find(i) == i的个数

连通分量个数为num,添加num-1条边可使图连通

#include<iostream>
#include<cstdio>

using namespace std;

const int MAX_N = 1005;

int father[MAX_N];    // 每个结点的父亲节点
int height[MAX_N];    // 每个结点在树中的高度

void Initial(int n){
    for(int i = 0; i <= n; i++){
        father[i] = i;
        height[i] = 0;
    }
}

int Find(int x){
    if(x != father[x]){
        father[x] = Find(father[x]);
    }
    return father[x];
}

void Union(int x, int y){
    x = Find(x);
    y = Find(y);
    if(x != y){ // 如果传入的两个点不是一个集合中的
        // 矮树作为高树的子树
        if(height[x] < height[y]){
            father[x] = y;
        }
        else if(height[x] > height[y]){
            father[y] = x;
        }
        else{
            father[y] = x;
            height[x]++;
        }
    }
    return;
}

int main(){
    int n, m;
    while(scanf("%d", &n) != EOF){
        if(n == 0) break;
        Initial(n);
        scanf("%d", &m);
        int a, b;
        while(m--){
            scanf("%d %d", &a, &b);
            Union(a, b);
        }
        int number = 0; // 集合的数目
        for(int i = 1; i <= n; i++){
            // 寻找根节点,即为树的个数,即为连通分量的个数
            if(Find(i) == i)
                number++;
        }
        // 该图有number个连通分量,需要添加number-1条边使图连通
        printf("%d\n", number - 1);
    }
    return 0;
}

用于求最小生成树的Prim算法(适使用稠密图)O(n2)

首先任取一个顶点加入S,然后每次选取一个与S中元素距离最近的点及边加入S中,最后得到最小生成树。

算法如下,其中priorityQ存的是所有T到所有S的点对及其距离,每次选一个T中最小的。priorityQ中的点可能重复,因为对于同一个点,它的距离是与多个S中的点之间的,只需在某一次作为最近的点被找出来即可。

struct Edge{
    int pos;
    int length;
    Edge(int p, int l): pos(p), length(l) {}
};
vector<Edge> graph[MAX_N];
struct Point{
    int to;
    int length;
    Point(int t, int l): to(t), length(l) {}
    bool operator< (const Point& p) const{
        return length > p.length;
    }
};
bool S[MAX_N];

int Prim(int n){
    int cost = 0, cnt = 0;
    priority_queue<Point> pointQ;
    pointQ.push(Point(1, 0));
    while(!pointQ.empty()){
        int u = pointQ.top().to;          // 22~28行:选取一个离S中点u最近的点加入S
        int d = pointQ.top().length;
        pointQ.pop();
        if(S[u]) continue;  // 如果该点已经被加入S了,只将其pop出去而不进行下面的操作
        S[u] = true;
        cnt++;
        cost += d;
        if(cnt == n) break; // 如果所有点都已经加入了S,结束算法
        for(int i = 0; i < graph[u].size(); i++){
            // 找到u后,将所有与u相连且不在S中的点加入优先队列,等待在以后的寻找中作为最近的点被加入S
            int v = graph[u][i].pos;
            int c = graph[u][i].length;
            if(S[v]) continue;
            priorityQ.push(Point(v, c));
        }
    }
    return total;
}

int main(){
    int n, m;
    scanf("%d %d", &n, &m);
    while(m--){
        int a, int b, int d;
        scanf("%d %d %d", &a, &b, &d);
        graph[a].push_back(Edge(b, d));
        graph[b].push_back(Edge(a, d));
    }
    fill(S, S+n+1, false);
    printf("%d", Prim(n));
    return 0;
}

用于求最小生成树的Kruskal算法(适用稀疏图)O(mlogm)

初始时S中包含所有的点,不包含任何一条边,每次选取一条权值最小的边,如果加入该边后S不含回路,则加入,如果含回路,则舍弃

并查集的使用方法:一开始所有结点都单独成树(Initial函数),如果两个结点连通且不成环,就将两个结点Union,最后得到一个并查集

struct Edge{
    int from;
    int to;
    int length;
    bool operator< (const Edge& e) const{
        return length < e.length;
    }
};

Edge edge_graph[MAX_N * MAX_N];
int father[MAX_N];
int height[MAX_N];   // height[x]存的是以结点x为根的树的高度

void Initial(int n){
    for(int i = 0; i <= n; i++){
        father[i] = i;
        height[i] = 0;
    }
}
// 查找:对于每个元素,不断向上查找,直到找到它的根,根据根是否相同来判断两个元素是否在同一个集合中
int Find(int x){
    if(x != father[x])
        father[x] = Find(father[x]);
    return father[x];
}
// 合并:将一棵树作为另一棵树的子树,从而将两棵树变成一棵更大的树
void Union(int x, int y){
    x = Find(x);
    y = Find(y);
    // 如果x高于y,将y作为x的子树
    if(height[x] > height[y]){
        father[y] = x;
    }
    // 如果y高于x,将x作为y的子树
    else if(height[x] < height[y]){
        father[x] = y;
    }
    // 如果两树同高,将y作为x的子树,此时x的高度会增加1
    else{
        father[y] = x;
        height[x]++;
    }
}

int Kruskal(int n, int m){
    Initial(n);
    sort(edge_graph, edge_graph + m); // 对所有边的长度进行排序
    int sum = 0;
    for(int i = 0; i < m; i++){
        Edge temp = edge_graph[i];
        if(Find(temp.from) != Find(temp.to)){  // 如果from和to目前不在同一个集合中,即不连通,则加入该边不会成环
            Union(temp.from, temp.to);
            sum += temp.length;
        }
    }
    return sum;
}

int main(){
    int n;
    while(scanf("%d", &n) != EOF){
        if(n == 0) break;
        int m = n * (n - 1) / 2;
        for(int i = 0; i < m; i++)
            scanf("%d %d %d", &edge_graph[i].from, &edge_graph[i].to, &edge_graph[i].length);
        printf("%d\n", Kruskal(n, m));
    }
    return 0;
}

AOV网拓扑排序(用于判断有向无权图是否有环)

static const int MAXN = 100005;
vector<int> graph[MAXN]; // graph[i]中为以i为起始的终点
int in_degree[MAXN]; // 存储每个节点的入度
bool TopologicalSort(int n){
    queue<int> node_in0;
    int num_sorted = 0;
    for(int i = 0; i < n; i++){
        // 找到所有入度为0的点
        if(in_degree[i] == 0) {
            node_in0.push(i);
        }
    }
    while(!node_in0.empty()){
        // 将入度为0的temp放入node_in0队列中
        int temp = node_in0.front();
        node_in0.pop();
        num_sorted++;
        // 更新temp连接的终点的入度(--)
        for(int i = 0; i < graph[temp].size(); i++){
            int v = graph[temp][i];
            in_degree[v]--;
            if(in_degree[v] == 0) {
                node_in0.push(v);
            }
        }
    }
    return num_sorted == n;
}
int main(){
    int n, m;
    scanf("%d %d", &n, &m);
    for(int i = 0; i < m; i++){
        int from, to;
        scanf("%d %d", &from, &to);
        graph[from].push_back(to);
        in_degree[to]++; // to的入度++
    }
    return TopologicalSort(numCourses);
}

AOE网关键路径

以顶点表示事件,以有向边表示活动,以边上权值表示该活动持续的时间。

从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,关键路径上的活动称为关键活动。

每个活动的

  • 最早开始时间:该活动的前序活动完成,该活动可以开始进行的时间
  • 最晚开始时间:该活动的后序活动需按时完成,该活动必须开始的时间

非关键活动可以拖延,不会影响整个工程的完成。而关键活动的拖延会影响整个工程的完成。因此求关键路径可以转化为求所有活动的最早和最晚开始时间,判断二者是否相等。

计算方式是:通过拓扑排序找出活动的前序和后序活动,那么活动的最早开始时间就是其所有前序活动的最晚完成时间,活动的最晚开始时间就是其所有后序活动的最早开始时间减去该活动所需时间。

#include<iostream>
#include<cstdio>
#include<vector>

using namespace std;

vector<int> graph[MAX_N];  // graph[i]存放结点i的后序结点
int inDegree[MAX_N];
long long earliest[MAX_N]; // 最早开始时间
long long latest[MAX_N];   // 最晚开始时间
long long time[MAX_N];     // 花费时间

long long CriticalPath(int n){
    vector<int> topology;  // 拓扑序列
    queue<int> node_in0;   // 存入度为0的点
    long long total_time = 0;
    for(int i = 1; i <= n; i++)
        if(inDegree[i] == 0)
            node_in0.push(i);
    while(!node_in0.empty()){
        int u = node_in0.front();
        topology.push_back(u);
        node_in0.pop();
        for(int i = 0; i < graph[u].size(); i++){
            // 遍历u的所有后序结点
            int v = graph[u][i];
            earliest[v] = max(earliest[v], earliest[u] + time[u]);
            inDegree[v]--;
            if(inDegree[v] == 0){
                node_in0.push(v);
                totalTime = max(totalTime, earliest[v] + time[v]);
            }
        }
    }
    for(int i = topology.size() - 1; i >= 0; i--){
        int u = topology[i];
        if(graph[u].size() == 0){
            // 如果u是最后的结点,在totalTime最后完成可以不耽误工期
            latest[u] = totalTime - time[u];
        }
        else{
            // 如果u不是最晚结点,将其初始化为INT_MAX
            latest[u] = INT_MAX;
        }
        // 遍历u的所有后序结点,寻找其中要求最早的
        for(int j = 0; j < graph[i].size(); j++){
            int v = graph[u][j];
            latest[u] = min(latest[u], latest[v] - time[u]);
        }
    }
    return totalTime;
}

int main(){
    int n, m;
    scanf("%d %d", &n, &m);
    memset(inDegree, 0, sizeof(inDegree));
    memset(earliest, 0, sizeof(earliest));
    memset(latest, 0, sizeof(latest));
    for(int i = 1; i <= n; i++)
        scanf("%lld", &time[i]);
    while(m--){
        int from, to;  // from是to的前序结点
        scanf("%d %d", &from, &to);
        graph[from].push_back(to);
        inDegree[to]++;
    }
    long long total_time = CriticalPath(n);
}

第12章 动态规划

走台阶问题

可以两步或一步

dp[n] = dp[n - 1] + dp[n - 2]

dp[1] = 1;

dp[2] = 2;

最大连续子序列和

用dp[i]表示以A[i]结尾的连续序列的最大和

那么这一序列要么是A[i]本身,要么是以A[i-1]结尾的最大连续序列再加上A[i]

即dp[i] = max(A[i], dp[i - 1] + A[i])

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<climits>

using namespace std;

int arr[1000005];
int dp[1000005];

int main(){
    int n;
    while(scanf("%d", &n) != EOF){
        memset(arr, 0, sizeof(arr));
        for(int i = 0; i < n; i++)
            scanf("%d", &arr[i]);
        dp[0] = arr[0];
        int max_num = dp[0];
        for(int i = 1; i < n; i++){
            dp[i] = max(arr[i], arr[i] + dp[i - 1]);
            if(dp[i] > max_num)
                max_num = dp[i];
        }
        printf("%d\n", max_num);
    }
}
最大子矩阵

动态规划思路:

控制行数,设最大子矩阵所在行是从i到j,然后将每一列从i到j元素求和,然后求最大序列和

#include<iostream>
#include<cstdio>
#include<cstring>
#include<climits>

using namespace std;

int matrix[105][105];  // 原始矩阵
int total[105][105];   // 辅助矩阵,[i, j]位置存放第j列中0-i行元素值
int arr[105];  // 转化为一维矩阵,arr中是第k列从i到j的元素和
int dp[105]; // 为arr进行动规的数组

// 对转化成的一维数组进行动态规划
int MaxSubSequence(int n){
    int max_num;
    dp[0] = arr[0];
    max_num = dp[0];
    for(int i = 1; i < n; i++){
        dp[i] = max(arr[i], dp[i - 1] + arr[i]);
        max_num = max(max_num, dp[i]);
    }
    return max_num;
}

// 将矩阵从i到j队列求和转化为一维数组
int MaxSubMatrix(int n){
    int max_num = INT_MIN;
    for(int i = 0; i < n; i++){
        for(int j = i; j < n; j++){
            // 控制行数,子矩阵从第i行到第j行
            for(int k = 0; k < n; k++){
                // 列为k
                if(i == 0){
                    arr[k] = total[j][k];
                }
                else{
                    arr[k] = total[j][k] - total[i - 1][k];
                }
                // arr中是第k列从i到j的元素和
            }
            int current = MaxSubSequence(n); // 转化为一维
            max_num = max(max_num, current);
        }
    }
    return max_num;
}

int main(){
    int n;
    while(scanf("%d", &n) != EOF){
        memset(matrix, 0, sizeof(matrix));
        memset(total, 0, sizeof(total));
        memset(arr, 0, sizeof(arr));
        memset(dp, 0, sizeof(dp));
        for(int i = 0; i < n; i++)
            for(int j = 0; j < n; j++)
                scanf("%d", &matrix[i][j]);
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                if(i == 0)
                    total[i][j] = matrix[i][j];
                else
                    total[i][j] = total[i - 1][j] + matrix[i][j];
            }
        }
        printf("%d\n", MaxSubMatrix(n));
    }
    return 0;
}

最长递增子序列

注意子序列和子串的区别:子串一定是连续的,而子序列不是

动规思路:

dp[i]初始化为1,即只有A[i]一个元素

遍历A[i]之前的所有元素,如果A[i]之前的元素A[j]比A[i]小,且dp[j]加上一个元素A[i]得到的子序列比dp[i]大,那么dp[i] = dp[j] + 1,否则dp[i] 不变。即dp[i] = max(dp[i], dp[j] + 1)

#include<iostream>
#include<cstdio>

using namespace std;

int arr[30];
int dp[30];

int main(){
    int n, max_num = 0;
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
        scanf("%d", &arr[i]);
    for(int i = 0; i < n; i++){
        dp[i] = 1;
        for(int j = 0; j < i; j++){
            if(arr[j] <= arr[i]){
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        max_num = max(max_num, dp[i]);
    }
    printf("%d\n", max_num);
    return 0;
}

最长公共子序列

给定两个字符串S1和S2,求这两个字符串的公共子串

动规思路:

dp为二维数组,dp[i][j]表示以S1[i]为末尾的字符串和以S2[j]为末尾的字符串的最长公共子序列的长度。

如果S1[i] = S2[j],则dp[i][j] = dp[i-1][j - 1] + 1

如果S1[i] != S2[j],则dp[i][j] = max(dp[i-1][j], dp[i][j-1])

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

char s1[105], s2[105];
int dp[105][105];

int main(){
    while(scanf("%s%s", s1 + 1, s2 + 1) != EOF){ // 从1开始读入
        memset(dp, 0, sizeof(dp));
        int len1 = strlen(s1 + 1);
        int len2 = strlen(s2 + 1);
        int max_len = 0;
        for(int i = 1; i <= len1; i++) // 从1遍历到len1
            for(int j = 1; j <= len2; j++){
                if(s1[i] == s2[j]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }
                else{
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
                }
            }
        printf("%d\n", dp[len1][len2]);
    }
    return 0;
}

背包问题

0-1背包

每种物品只有一个,物品重量为w[i],价值为v[i],让总价值最大

dp[i][j]:前i个物品装进容量为j的背包能获得的最大价值

动规思路:

每个物品只有装进和不装进两种选择。如果不装进,dp[i][j] = dp[i-1][j];如果装进,dp[i][j] = dp[i-1][j-w[i]]+v[i]。

依次遍历i和j即可。

优化思路:

滚动数组。如果装进物品i,dp[j] = dp[j-w[i]] + v[i],如果不装进dp[j] = dp[j]。

第二重循环倒序目的是使每个物品只能取一次。倒序时,当空间为2*w[j]时dp[j] = dp[j - w[j]] + v[i],此时1个w[j]处还没遍历到,值仍为0,2个w[j]得到的价值仍为1个物品的价值v[i]

int w[MAX_N]; // 物品的重量
int v[MAX_N]; // 物品的价值
int dp[MAX_W]; // 背包容量为j时的最大价值
int main(){
    int W, N; // 背包容量和物品数量
    scanf("%d%d", &W, &n);
    for(int i = 0; i < n; i++)
        scanf("%d%d", &w[i], &v[i]);
    memset(dp, 0, sizeof(dp));
    for(int i = 0; i < n; i++)
        for(int j = W; j >= w[i]; j--)
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
    printf("%d\n", dp[W]);
    return 0;
}
完全背包

每个物品可以取无限个,求背包可以获得的最大价值

将第二重循环的倒序改为正序即可。当空间为2*w[j]时,正序遍历一个w[j]位置的值为v[i],2个w[j]的值为2个v[i]

int w[MAX_N]; // 物品的重量
int v[MAX_N]; // 物品的价值
int dp[MAX_W]; // 背包容量为j时的最大价值
int main(){
    int W, N; // 背包容量和物品数量
    scanf("%d%d", &W, &n);
    for(int i = 0; i < n; i++)
        scanf("%d%d", &w[i], &v[i]);
    memset(dp, 0, sizeof(dp));
    for(int i = 0; i < n; i++)
        for(int j = w; j <= W; j++)
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
    printf("%d\n", dp[W]);
    return 0;
}
多重背包

每个物品可以取有限个,求背包可以获得的最大价值

思路:将物品的有限值num转化为num=1+2+22+…+2k+剩下的。将一个物品转化为k+1种数量为1的物品,物品价值依次为1×w、2×w、……、2k×w、剩余值×w,这样可以保证任何组合都等价。

int w[MAX_N]; // 物品的重量
int v[MAX_N]; // 物品的价值
int dp[MAX_W]; // 背包容量为j时的最大价值
int main()
{
	int W, N;
	scanf("%d%d", &W, &n); 
	int cnt = 0;
    int weight, value, count;
    for(int i = 0; i < n; i++){
        scanf("%d%d%d", &weight, &value, &count);
        for(int j = 1; j <= count; j *= 2){
            v[cnt] = j * value;
            w[cnt] = j * weight;
            cnt++;
            count -= j;
        }
        if(count > 0){
            v[cnt] = count * value;
            w[cnt] = count * weight;
            cnt++;
        }
    }
    memset(dp, 0, sizeof(dp));
    for(int i = 0; i < cnt; i++)
        for(int j = M; j <= w[i]; j--)
            dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
    printf("%d\n", dp[M]);
}
组合背包

输入数量为-1代表无限取用

输入时,如果是无限取用,直接将-1存入count数组中。如果是有限取用,转化为01背包。

遍历时,如果是无限取用,正序遍历,如果是01背包,倒序遍历。

int w[MAX_N]; // 物品的重量
int v[MAX_N]; // 物品的价值
int c[MAX_N]; // 物品的数量
int dp[MAX_W]; // 背包容量为j时的最大价值
int main()
{
	int W, N;
	scanf("%d%d", &W, &n); 
	int cnt = 0;
    int weight, value, count;
    for(int i = 0; i < n; i++){
        scanf("%d%d%d", &weight, &value, &count);
        if(count == -1){
            w[cnt] = weight;
            v[cnt] = value;
            c[cnt] = count;
            cnt++;
        }
        else{
            for(int j = 1; j <= count; j *= 2){
            	v[cnt] = j * value;
            	w[cnt] = j * weight;
                c[cnt] = 1;
            	cnt++;
            	count -= j;
        	}
       		if(count > 0){
            	v[cnt] = count * value;
            	w[cnt] = count * weight;
                c[cnt] = 1;
            	cnt++;
        	}
        }
    }
    for(int i = 0; i < n; i++){
        if(c[i] == -1){
            // 如果是无限取用的
            for(int j = w[i]; j <= W; j++)
                dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
        else{
            // 如果是01背包
            for(int j = W; j >= w[i]; j--)
                dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    printf("%d\n", dp[W]);
}
恰好装满

思路:初始化的不同,只有容量为0时可以装满,此时价值为0,其他位置都初始化为INF(或-1),表示不能恰好装满。

在进行max操作的时候,需要判断dp[j-w[i]]是否大于等于0,只有大于等于0的才能参与运算。

int w[MAX_N]; // 物品的重量
int v[MAX_N]; // 物品的价值
int dp[MAX_W]; // 背包容量为j时的最大价值
int main(){
    int W, N; // 背包容量和物品数量
    scanf("%d%d", &W, &n);
    for(int i = 0; i < n; i++)
        scanf("%d%d", &w[i], &v[i]);
    memset(dp, INF, sizeof(dp));
    dp[0] = 0;
    for(int i = 0; i < n; i++)
        for(int j = w; j <= W; j++)
            if(dp[j-w[i]] >= 0)
            	dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
    printf("%d\n", dp[W]);
    return 0;
}

动态规划注意:如果dp[0]在for循环外单独赋值的话,注意max_num应初始化为dp[0],而不是INT_MIN

卡特兰数

满足h[n] = h[0]*h[n-1] + h[1]*h[n-2] + …… + h[n-2]*h[1] + h[n-1]*h[0]

int CatalanNumber(int n){
    int dp[25]; // dp[i]表示i个数的排列种数
    memset(dp, 0, sizeof(dp));
    dp[0] = 1; // 0个数只有一种
    dp[1] = 1; // 1个数也只有一种
    for(int i = 2; i <= n; i++){ // 从2个数开始遍历,直到遍历到n
        for(int j = 1; j <= i; j++){ // j为根节点,对于i个数,根节点从1遍历到i,dp[i]j
            // 对于i个数,以j为根节点,则左子树有j-1个,右子树有i-j个
            dp[i] += dp[j - 1] * dp[i - j];
        }
    }
    return dp[n];
}

卡特兰数可以解决的问题:

括号化

凸多边形三角划分

给定节点组成二叉搜索树

n对括号的正确匹配数目

技巧

输入一个int型数字,求其逆序数

int reverse(int input) {
    int output = 0;
    while (input != 0) {
        output *= 10;
        output += input % 10;
        input /= 10;
    }
    return output;
}

如果所有数字都是int型,只有一个地方出现了除法或浮点数,可以通分简化计算,例如

int x, y, n;
if(x * 5 + (double)y / 3 <= n)    ×
if(x * 5 * 3 + y <= n * 3)        √

格式化输入输出

输入:

// YYYYMMDD的读入
scanf("%4d%2d%2d", &year, &month, &day);

格式化输出

int d = 1;
printf("%3d", d); // 右对齐输出  1
printf("%03d", d); // 右对齐,左补零,输出0

加速cin、cout

ios::sync_with_stdio(false);

用cin或scanf输入字符串时,遇到空格会停止读入,如果想读入包含空格的一行,可以使用:

string str;
getline(cin, str);

C++结构体重载小于号

在sort、set、map中,默认排序方式是小顶堆,即从小到大

struct node{
    int data;
    bool operator< (const node& n) const{
        return data < n.data;
    }
}
node list[100];
sort(list, list + 20); // 此时是从小到大排序

在优先队列中,默认排序方式是大顶堆,因此重载小于号时要反号才能从小到大排序

struct node{
    int data;
    bool operator< (const node& n) const{
        return data > n.data;  // 反号
    }
}
priority_queue<node> q;  // 反号才能从小到大排序

差分数组

当从1到10000区间中每一个数都需要加1(或任何值)时,可以使用差分数组。

差分数组d[i] = arr[i] - arr[i - 1],d[0] = 0

当从L到R都需要加一个相同值x时,区间中间从L+1到R-1部分的差是不变的,只有:

d[L] = arr[L] - arr[L-1]变为了arr[L]+x-arr[L-1]=d[L]+x

d[R+1] = arr[R+1] - arr[R]变为了arr[R+1]-(arr[R]+x)=d[R+1]-x

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值