递归与递推

会独立敲一遍代码并debug(1,3题较难;2,4题较简单)

部分题需要买课,可到洛谷或其他OJ找原题

目录

空间复杂度(计算方法)

1,费解的开关

2,带分数

3,飞行员兄弟

4,翻硬币


空间复杂度(计算方法)

1byte(字节) = 8bit(位),位指二进制位

float = int = 4byte = 32bit

char = 1byte = 8bit

double = long long = 8byte = 64bit

递归与递推_第1张图片

已知bite = B, bit = b

64MB ≈ 64 * 1e6(B),M是兆,1G = 1024M,M也是百万的缩写

= 64 * 2^20(B) ≈ 6.4 * 10^7(B) = 1.6 * 10^7(int),即 1600万个int

考虑到额外占用的空间,一般开到1500万,换算成2维数组就是a[3800][3800]

1,费解的开关

标签:递推,位运算,中等

95. 费解的开关 - AcWing题库

视频

AcWing 95. 费解的开关(蓝桥杯C++ AB组辅导课) - AcWing

题解 

AcWing 95. 费解的开关 - AcWing

位运算(目录 -- biset) 

STL入门 + 刷题(下)_千帐灯无此声的博客-CSDN博客

注意将 step = 0 和 memcpy() 放在 for(int op...) 里,因为32个种op代表第一行的32种方案数,每个op都要重新备份,都有自己的step

当你每次结果都输出 -1 时,就应该想到 step 或者 ret 的问题

AC  代码

#include
#include //memcpy()
using namespace std;
#define N 6

char m[N][N], backup[N][N]; //m[][]表示亮或不亮

int tx[5] = {0,1,0,-1,0}, ty[5] = {1,0,-1,0,0}; //上下左右中, 漏了中间

void turn(int a, int b) //对(a, b)所在点和上下左右共5个点操作
{
    for(int i = 0; i < 5; ++i) {
        int aa = a + tx[i], bb = b + ty[i];
        if(aa < 0 || aa > 4 || bb < 0 || bb > 4)
            continue; //越界
        //1^1 == 0, 0^1 == 1
        m[aa][bb] ^= 1; //等价于m[aa][bb] = m[aa][bb] ^ 1
    }
}

int main()
{
    int t;
    cin>>t;
    while(t--) {
        int ret = 10; //最小步数, 初始取尽可能的大

        for(int i = 0; i < 5; ++i)
            cin>>m[i]; //每一行按字符串输入到m[][], 表示亮或不亮

        for(int op = 0; op < 32; ++op) { //op表示第 0 行 按 / 不按
            
            //m拷贝到backup, 便于32组方案每组的恢复
            memcpy(backup, m, sizeof(m));
            
            int step = 0; //当前步数
            
            //操作第1行
            for(int i = 0; i < 5; ++i)  //比如op是13, 对应01101, 1按, 0不按
                if(op >> i & 1) { //右移 i 位 与 1 得到第 i 位的数字
                    step++;
                    turn(0, i); //第1行第i + 1列
                }
            //根据1234行操作2345行
            for(int i = 0; i < 4; ++i) //1234行
                for(int j = 0; j < 5; ++j)
                    if(m[i][j] == '0') { //主要是char
                        step++;
                        turn(i + 1, j);
                    }
            //是否能全部亮
            bool dark = false;
            for(int j = 0; j < 5; ++j)
                if(m[4][j] == '0') { //注意是char, 不要直接!
                    dark = true;
                    break;
                }
            if(!dark) ret = min(ret, step);
            memcpy(m, backup, sizeof(m)); //不要忘记拷贝回去
        }
        if(ret <= 6) cout<

复杂度

由代码可知,32 * 25 * 5 * 500 = 2e6

1,第一行的 op 有32种方案

2,然后在32种方案的前提下,根据op对应的二进制对第一行操作,这里有5种

3,接着根据1~4行对2~5行操作,这里20种(5 + 20 = 25)

4,根据 turn() 函数,每个点要操作5次

5,最多500组测试

2,带分数

标签:递归,搜索,剪枝

1209. 带分数 - AcWing题库

解法 -- 暴力

暴力的2种解法都是3000多ms

(1)全排列打乱1~9的顺序

(2)cal()函数得到对应的整数,比如345896712这个排列里,取第1~4位就会得到4589

(3)两层for循环遍历(两个分割点),以便得到a, b, c对应的位数

(4)最后判断满足条件,将除法转化为乘法,避免C++对小数的处理

复杂度

全排列最坏复杂度 < n! * n,视作 n! * n,加上2层for循环枚举位数,相当于9个数里插2个隔板,即 C_{8}^{2} = 28,已知 n = 9(9个数字全排列),

那么时间复杂度(运算次数) = 28 * 9! * 9 ≈ 1e8,刚好不超时

关于 next_permutation( 进入文章后,左侧目录跳转STL常用函数  第5个的next_permutation 

STL入门 + 刷题(下)_千帐灯无此声的博客-CSDN博客

AC  代码1

全排列 -- next_permutation() 

#include
#include // next_permutation()
using namespace std;

int num[9] = {1,2,3,4,5,6,7,8,9};

int cal(int i, int j) // 比如 i=3,j = 5, 全排列前返回345
{
    int res = 0;
    for (int x = i - 1; x < j; ++x)
        res = res * 10 + num[x];
    return res;
}

int main()
{
    int target, a, b, c, ans = 0; // target = a + b/c
    cin>>target;
    do {
        // 两层for枚举左右边界(a, b, c的位数)
        for (int l = 1; l <= 7; ++l) // a=num[..l] b=num[l+1..r] c=num[r+1..]
            for (int r = l + 1; r <=8; ++r) {
                a = cal(1, l);
                b = cal(l+1, r);
                c = cal(r+1, 9);
                if (target * c == a * c + b) // 除法转乘法
                    ans++;
            }
    }while(next_permutation(num, num + 9)); // 全排列打乱顺序
    
    cout<

AC  代码2

dfs 深度优先搜索,手写全排列

#include
using namespace std;

int num[9], vis[9];
int ans = 0, a, b, c, target;

int cal(int i, int j) // 比如 i=3,j = 5, 全排列前返回345
{
    int res = 0;
    for (int x = i - 1; x < j; ++x)
        res = res * 10 + num[x];
    return res;
}

void dfsabc() // 分配位数
{
    for (int l = 1; l <= 7; ++l)
        for (int r = l + 1; r <= 8; ++r) {
            a = cal(1, l);
            b = cal(l + 1, r);
            c = cal(r + 1, 9);
            if (target * c == a * c + b) 
                ans++;
        }
}

void dfs(int u) // dfs 全排列, u表示递归的深度(第u个数)
{
    if (u > 9) {
        dfsabc();
        return;
    }
    for (int i = 0; i <= 8; ++i) 
        if(!vis[i]) { // 未访问过
            num[u - 1] = i + 1; // u从1开始, i从0开始
            vis[i] = 1; // 标记
            dfs(u + 1); // 递归
            vis[i] = 0; // 取消标记
        }
}

int main()
{
    cin>>target;
    dfs(1);
    cout<

解释下第35行

num[u - 1] = i + 1; // u从1开始, i从0开始

dfs(int u)里的u表示递归的深度,即第 u 位数,因为 u 从1开始

i 从 0 开始,所以 num[u - 1] = i + 1

3,飞行员兄弟

标签:枚举,位运算

116. 飞行员兄弟 - AcWing题库

视频讲解

AcWing 116. 飞行员兄弟 - AcWing

暴力解法

暴力解法,分数组和二进制优化,2份AC代码,前者用数组存储棋盘,后者用二进制存储棋盘

解释 

(1)每个位置最多操作1次(操作2次相当于没操作),最终结果与操作的顺序无关

(2)操作一个位置,所在的 行 和 列 全部反转

(3)采取暴力枚举

一共16个开关,每个开关摁或不摁2种可能,所以是 2^16 种方案

每个方案有16个开关要遍历,采取位运算的话,对7个位置(同一行 / 列)只需要操作一次

所以时间复杂度是 2^16 * 16 = 2^20 ≈ 1e6

----  当然,如果不用二进制优化,直接用数组存棋盘,复杂度 2^16 * (16 * 7 + 16 + 16)  ----

----  7表示每个开关共关联7个开关(同一行 / 列),两个16表示检查16个位置是否都开,以及都开的话,要记录结果  ----

(4)将4*4棋盘,按字符串读入4次,然后将读入的棋盘转化成一个整数(这个整数表示一个16位的二进制数),关于位运算

c++之位运算(详解,初学者绝对能看懂)_c++位运算_?!??的博客-CSDN博客

(5)常用技巧(判断一个二进制数 num 第 i 位是0还是1,第0位即低位第一位),只需要

num >> i & 1

(6)关于题目中的 “如果存在多种打开冰箱的方式,则按照优先级整体从上到下,同行从左到右打开”,只需要输出 0 ~ 2^16 - 1 里面字典序最小的方案(16位二进制中,1越靠前的方案,字典序越小),只需要从0开始遍历

常用技巧

 位运算常用技巧:用一个整数存储一个矩阵或者一维数组的信息(整数对应的二进制)

思路

1,棋盘的16个数字读入字符数组 g[][]

2,枚举 0 ~ 2^16 - 1 这 2^16 种方案 

3,每种方案,2层for循环遍历行 / 列,进行操作

补充 

(1)

为什么2^16种操作数里,每种操作数表示一种操作方案呢

一个操作数,比如6,的二进制表示为0000 0000 0000 0110

又比如2^16-1,二进制表示为1111 1111 1111 1111

这里的1表示需要操作,即开关是关着的,需要被打开

(2)

数组存棋盘,将 + 和 - 字符读入char[][]里,同时,为了判断某一种操作方案16个位置,某个位置是否需要打开,想象棋盘为

0  1  2  3  

4  5  6  7

8  9  10 11

12 13 14 15

只需要 操作数 << 棋盘数字,再 & 1,即可得到该位置是否需要打开

(3)

关于memcpy

std::memcpy - cppreference.com

// Defined in header 
void* memcpy( void* dest, const void* src, std::size_t count );

第 2 个参数,拷贝到第 1 个参数里,第 3 个参数为容器大小

(4)

关于pair

std::pair - cppreference.com

(5)

关于vector中operator=

std::vector::operator= - cppreference.com

(6)

关于for循环中的auto

Range-based for loop (since C++11) - cppreference.com

AC  数组

递归与递推_第2张图片    op存储操作方案,op对应的二进制中,1表示需要操作,g[][]存储初始状态

#include
#include
#include
#include // memcpy()
using namespace std;

#define x first
#define y second

typedef pair PII;
const int N = 5;

char g[N][N], backup[N][N]; // 存储棋盘,拷贝
vector ans; // 存储答案

int get(int x, int y)
{
    return x * 4 + y; // 返回第x行第y列的数字0~15
}

void turn_one(int x, int y) 
{
    if (g[x][y] == '-') g[x][y] = '+';
    else g[x][y] = '-'; // 改变一个开关
}

void turn_all(int x, int y) // 改变所有关联开关
{
    for (int i = 0; i < 4; ++i) {
            turn_one(x, i); // 列
            turn_one(i, y); // 行
    }
    turn_one(x, y); // 翻转了2次,多了1次
}

int main()
{
    // 读入棋盘
    for (int i = 0; i < 4; ++i)
        cin>>g[i]; // 读入一行字符
        
    // 遍历2^16种操作(数)方案
    for (int op = 0; op < 1 << 16; ++op) {
        memcpy(backup, g, sizeof g); // 备份: g拷贝到backup
        vector temp; // 存储当前所有操作
        // 枚举当前操作方案的16个开关
        for (int i = 0; i < 4; ++i)
            for (int j = 0; j < 4; ++j) 
                if (op >> get(i, j) & 1) { // 需要翻转
                    temp.push_back({i, j}); // 插入操作
                    turn_all(i, j); // 按一下开关
                }
                
        // 检查是否全部打开
        bool if_close = false;
        for (int i = 0; i < 4; ++i)
            for (int j = 0; j < 4; ++j)
                if (g[i][j] == '+')
                    if_close = true; 
        if (!if_close && (ans.empty() || ans.size() > temp.size())) 
            ans = temp; // 更新最小切换次数
        
        // 不要忘记备份回来, turn_all会修改原棋盘
        memcpy(g, backup, sizeof g);
    }
    
    cout<

AC  二进制

递归与递推_第3张图片   比直接用数组存快了一倍

#include
#include
using namespace std;

#define x first
#define y second
typedef pair PII;

int change[5][5];

int get(int x, int y)
{
    return x * 4 + y;
}

int main()
{
    int state = 0; // state 存储棋盘初始状态
                   // 二进制中1表示关,0表示开
    // 读入
    for (int i = 0; i < 4; ++i) {
        string line; // 读入4行字符
        cin>>line;
        for (int j = 0; j < 4; ++j) 
            if (line[j] == '+') // 存下初始状态
                state += 1 << get(i, j); // +表示对应二进制位上的1
    }
    
    // 预处理一个开关以及同一行/列的所有数
    for (int i = 0; i < 4; ++i)
        for (int j = 0; j < 4; ++j) {
            for (int k = 0; k < 4; ++k) {
                // 注意3个都是 [i][j]
                change[i][j] += 1 << get(i, k); // 行
                change[i][j] += 1 << get(k, j); // 列
            }
            change[i][j] -= 1 << get(i, j); // 减去重复部分
        }
    
    // 遍历
    vector ans; // 存储答案的每一步操作
    for (int op = 0; op < 1 << 16; ++op) { // op 存储要操作的开关
        vector temp;
        int now = state; // 初始状态拷贝
        for (int i = 0; i < 16; ++i)  // 遍历16个开关,对应二进制的16位
            if (op >> i & 1) { // 这个开关需要操作
                int x = i / 4, y = i % 4; // 得到行 列
                now ^= change[x][y]; // 需要操作的数取反
                temp.push_back({x, y}); // 记录操作每一步
            }
        if (!now && (ans.empty() || ans.size() > temp.size()))
            ans = temp; // 满足更少操作次数
        
    }
    
    // 输出答案
    cout<

4,翻硬币

1208. 翻硬币 - AcWing题库

标签:递推

思路

每次只改变相邻两个字符,由贪心可知,我们从第一个字符开始,如果第一个字符不一样,只能通过改变前2个字符来实现,由此可知,答案显然是唯一的

---- 从第一个字符遍历到最后,时间复杂度 O(n)

BUG

一开始,我用begin, end作为起始和目标字符串,报错,

error: reference to 'begin' is ambiguous

它与 C++ 标准库中的 begin 函数名产生了冲突

将begin改为Begin即可

AC 代码

#include
using namespace std;

string Begin, End;

void change(int x)
{
    Begin[x] = Begin[x] == '*' ? 'o' : '*'; // 三目运算符
    Begin[x + 1] = Begin[x + 1] == '*' ? 'o' : '*';
}

int main()
{
    cin>>Begin>>End;
    
    int ans = 0;
    for (int i = 0; i < Begin.size() - 1; ++i) 
        if (Begin[i] != End[i]) {
            change(i);
            ans++;
        }
    cout<

你可能感兴趣的:(2024蓝桥杯备赛,算法,蓝桥杯)