蓝桥杯C++AB算法辅导

文章目录

1.1 教学计划与递归

很不错的解释

在这里插入图片描述

92. 递归实现指数型枚举

从 1∼n 这 n 个整数中随机选取任意多个,输出所有可能的选择方案。

输入格式
输入一个整数 n。

输出格式
每行输出一种方案。

同一行内的数必须升序排列,相邻两个数用恰好 1 个空格隔开。

对于没有选任何数的方案,输出空行。

本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。

数据范围
1≤n≤15
输入样例:
3
输出样例:

3
2
2 3
1
1 3
1 2
1 2 3

//y总
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 16;

int n;
int st[N];  // 状态,记录每个位置当前的状态:0表示初始还没判断,1表示选它,2表示不选它

void dfs(int u)
{
    if (u > n)//u从1开始, 当n个位置确定时 u == n + 1 
    {
        for (int i = 1; i <= n; i ++ )
            if (st[i] == 1)
                printf("%d ", i);
        printf("\n");
        return;
    }

    st[u] = 2;
    dfs(u + 1);     // 第一个分支:不选
    st[u] = 0;  // 恢复现场

    st[u] = 1;
    dfs(u + 1);     // 第二个分支:选
    st[u] = 0;
}

int main()
{
    cin >> n;

    dfs(1);//从1开始

    return 0;
}


#include<iostream>
using namespace std;
int n;
//用二进制状态压缩,也就是用二进制上的位来记录数有没有被用过。
// u是当前枚举到的数,state是二进制数记录哪些数被选
void dfs(int u ,int state){
    if( u == n){
        for(int i = 0; i< n; i++)
            //判断第i位是不是1,如果是1就代表被选,输出
            if(state >> i & 1) 
                //第几位就代表输出几,只不过不是从0开始,而是从1开始
                cout << i + 1 << " ";
        cout << endl;
        return;
    }
    // 不用这个数,第u位不动
    dfs(u + 1, state); 
    // 用这个数,把第u位变成1
    // 运算优先级: 左移高于位运算|
    dfs(u + 1, state | 1 << u);  


}

int main()
{

    cin >> n;
    /*
    回顾一下dfs参数的含义:
        - u是当前枚举到的数,
        -state是二进制数记录哪些数被选

    则 dfs(0, 0)表示:当前枚举到0,没有数被选

    */ 
    dfs(0, 0);

}

//详细注释版
//作者:ShizhengLee 链接:https://www.acwing.com/solution/content/41249/



/*

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 20;

int n,a[N];

void dfs(int k){
    for(int i=1;i<k;++i)
        cout << a[i] << ' ';
    cout << endl;

    if(k > n)
        return;

    for(int i=a[k-1]+1;i<=n;++i){
        a[k] = i;
        dfs(k+1);
    }
}

int main()
{
    cin >> n;
    dfs(1);
    return 0;
}
*/

94. 递归实现排列型枚举

把 1∼n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。

输入格式
一个整数 n。

输出格式
按照从小到大的顺序输出所有方案,每行 1 个。

首先,同一行相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。

数据范围
1≤n≤9
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
在这里插入图片描述

全排列满足字典序最小,根据递归第一轮满足字典序最小之后也最小

全排列:dfs基础

#include<iostream>

using namespace std;

const int N = 10;
int path[N];
bool st[N];
int n;
void dfs(int u)//当前位u
{
    if(u == n)
    {
        for(int i = 0;i < n;i++)printf("%d ",path[i]);
        puts("");
    }
    
    for(int i = 1;i <= n;i++)
    {
        if(!st[i])
        {
            st[i] = true;
            path[u] = i;
            dfs(u + 1);
            path[u] = 0;//恢复现场,但其实path因为会被覆盖,所以这步可以省略【简单理解:全排列枚举所有结果就要恢复现场】
            st[i] =false;
        }
    }
}

int main()
{
    
    cin >> n;
    
    dfs(0);
        
    return 0;
}

717. 简单斐波那契

以下数列 0 1 1 2 3 5 8 13 21 ... 被称为斐波纳契数列。

这个数列从第 3 项开始,每一项都等于前两项之和。

输入一个整数 N,请你输出这个序列的前 N 项。

输入格式
一个整数 N。

输出格式
在一行中输出斐波那契数列的前 N 项,数字之间用空格隔开。

数据范围
0<N<46
输入样例:
5
输出样例:
0 1 1 2 3
存储Fib版

#include<iostream>
using namespace std;

const int N = 50;
int f[N];
int n;
int main()
{
    cin >> n;
    f[1] = 0 ,f[2] = 1;//下标从1开始 (也可以从0开始)
    for(int i = 3;i <= n;i++)f[i] = f[i - 1] + f[i - 2];
    
    for(int i = 1 ;i <= n;i ++)printf("%d ",f[i]);
    
    return 0;
}

不开数组版:滚动数组 - dp雏形
注意Fib当n超过46就需要开高精度了

#include<stdio.h>
typedef long long LL;
LL n,a = 0, b = 1, c;//a第一项,b第二项,下一项c = a + b , a,b位置再往后移动
int main()
{
    scanf("%d",&n);
    while(n--)
    {
        printf("%d ",a); //a每轮表示第i项  从1开始
        c=a+b;
        a=b,b=c;
    }
}

95. 费解的开关

你玩过“拉灯”游戏吗?

25 盏灯排成一个 5×5 的方形。

每一个灯都有一个开关,游戏者可以改变它的状态。

每一步,游戏者可以改变某一个灯的状态。

游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。

我们用数字 1 表示一盏开着的灯,用数字 0 表示关着的灯。

下面这种状态

10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:

01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:

01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6 步以内使所有的灯都变亮。

输入格式
第一行输入正整数 n,代表数据中共有 n 个待解决的游戏初始状态。

以下若干行数据分为 n 组,每组数据有 5 行,每行 5 个字符。

每组数据描述了一个游戏的初始状态。

各组数据间用一个空行分隔。

输出格式
一共输出 n 行数据,每行有一个小于等于 6 的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。

对于某一个游戏初始状态,若 6 步以内无法使所有灯变亮,则输出 −1。

数据范围
0<n≤500
输入样例:
3
00111
01011
10001
11010
11100

11101
11101
11110
11111
11111

01111
11111
11111
11111
11111
输出样例:

3
2
-1

核心:上一行的状态能确定下一行的状态
枚举前n-1行即可确定全部状态[前n-1规定每行全变亮, 则最后只需判断最后一行是不是全亮]
(先把第一行的状态拆分二进制,则有 2 5 2^5 25的分支去转移到下一行)


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

using namespace std;

const int N = 6;//字符串末尾'\0''

char g[N][N], backup[N][N];
int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0};//上下左右+自身

void turn(int x, int y)//相邻十字型翻转: (x, y)以及上下左右5个位置翻转
{
    for (int i = 0; i < 5; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;   // 在边界外, 直接忽略即可
        g[a][b] ^= 1;//字符类型的0与1互转【异或】: ASCLL码中0为48, 1为49 二进制仅最后一位1不同-->'0'^1 = '1'; '1'^1= 0 
    }
}

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        for (int i = 0; i < 5; i ++ ) scanf("%s", g[i]);//字符串读入

        int res = 10;
        for (int op = 0; op < 32; op ++ )//枚举第一行5个灯按或不按 [2^5种操作方式]
        {
            memcpy(backup, g, sizeof g);//每轮需要恢复成原始状态g, 先备份到backup
            int step = 0;
            for (int i = 0; i < 5; i ++ )//按下去就改变自身和上下左右四个格子, 即翻转5个格子
                if (op >> i & 1)//第一行有暗的就要按
                {
                    step ++ ;
                    turn(0, i);
                }

            for (int i = 0; i < 4; i ++ )//枚举确定的第1行以及234行,使得1234全亮
                for (int j = 0; j < 5; j ++ )
                    if (g[i][j] == '0')
                    {
                        step ++ ;
                        turn(i + 1, j);//中间行按法:如果这个位置是灭的,就按下一行对应的位置
                    }

            bool dark = false;//标记是否有暗的灯 
            for (int i = 0; i < 5; i ++ )//遍历最后一行判断是否全亮
                if (g[4][i] == '0')
                {
                    dark = true;//最后一行有暗的, 此轮失败
                    break;
                }

            if (!dark) res = min(res, step);//若最后一行全亮, 判断更新答案
            memcpy(g, backup, sizeof g);//判断下一轮状态, 恢复初始g
        }

        if (res > 6) res = -1;//限制5步, 超过5步返回-1

        cout << res << endl;
    }

    return 0;
}

1.2 递推与递归——习题课

93. 递归实现组合型枚举

从 1∼n 这 n 个整数中随机选出 m 个,输出所有可能的选择方案。

输入格式
两个整数 n,m ,在同一行用空格隔开。

输出格式
按照从小到大的顺序输出所有方案,每行 1 个。

首先,同一行内的数升序排列,相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面(例如 1 3 5 7 排在 1 3 6 8 前面)。

数据范围
n>0 ,
0≤m≤n ,
n+(n−m)≤25
输入样例:
5 3
输出样例:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5
思考题:如果要求使用非递归方法,该怎么做呢?

在这里插入图片描述

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

using namespace std;

const int N = 30;

int n, m;
int way[N];

void dfs(int u, int start)//当前位置下标u,当前选择的数值start【为了满足字典序】
{	//可加剪枝优化但不是记忆点 (挖掘条件得出):开始start到结束n长度不足剩下要填未确定的位置m - u
    if(n - start < m - u) return;  // 剪枝优化【减少时间,不影响结果】
    if (u == m + 1)
    {
        for (int i = 1; i <= m; i ++ ) printf("%d ", way[i]);
        puts("");
        return;
    }

    for (int i = start; i <= n; i ++ )
    {
        way[u] = i;
        dfs(u + 1, i + 1);//下一位,条件:下一个值比当前位值i大,即i+1(从小到大即可满足字典序)
        way[u] = 0; // 恢复现场【可不写,因为下一次覆盖上一次的值】
    }
}

int main()
{
    scanf("%d%d", &n, &m);

    dfs(1, 1);

    return 0;
}

1209. 带分数

100 可以表示为带分数的形式:100=3+69258714
还可以表示为:100=82+3546197
注意特征:带分数中,数字 1∼9 分别出现且只出现一次(不包含 0)。

类似这样的带分数,100 有 11 种表示法。

输入格式
一个正整数。

输出格式
输出输入数字用数码 1∼9 不重复不遗漏地组成带分数表示的全部种数。

数据范围
1≤N< 1 0 6 10^6 106
输入样例1:
100
输出样例1:
11
输入样例2:
105
输出样例2:
6

//y总思路:全排列 + 划分枚举a、c,判断b是否成立 ,等式 n == a + b / c 
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10;

int n;
bool st[N], backup[N];
int ans;

bool check(int a, int c)//检查b,等式是否成立
{	//可以开个全局LL
    long long b = n * (long long)c - a * c;  //n = a + b / c 鉴于int向上取整,两边同乘c避开int特性: n * (long long c) == a * c + b

    if (!a || !b || !c) return false;  //a,b,c中含有0,false

    memcpy(backup, st, sizeof st); //使用备份数组,复制原标记数组
    while (b)
    {
        int x = b % 10;     // 取个位
        b /= 10;    // 个位删掉
        if (!x || backup[x]) return false; //x不为0且x被标记过(a或c已经使用),则b不能选此数,false
        backup[x] = true;
    }

    for (int i = 1; i <= 9; i ++ )//1-9没有全部用上,false
        if (!backup[i])
            return false;

    return true;
}

void dfs_c(int u, int a, int c) //当前位u , a的值 , c的值
{
    if (u > 9) return;

    if (check(a, c)) ans ++ ;

    for (int i = 1; i <= 9; i ++ )
        if (!st[i])
        {
            st[i] = true;
            dfs_c(u + 1, a, c * 10 + i);
            st[i] = false;
        }
}

void dfs_a(int u, int a)//枚举a 
{
    if (a >= n) return; //a > n 等式不成立,剪枝
    if (a) dfs_c(u, a, 0); 

    for (int i = 1; i <= 9; i ++ )
        if (!st[i])
        {
            st[i] = true;
            dfs_a(u + 1, a * 10 + i); //判断下一组a,a加位数
            st[i] = false;
        }
}

int main()
{
    cin >> n;

    dfs_a(0, 0);

    cout << ans << endl;

    return 0;
}

next_permutation

#include <bits/stdc++.h>
using namespace std;
int num[10] = {1,2,3,4,5,6,7,8,9};  //[1-9]
int cnt;

int get(int l,int r)  //区间[l, r]组成一个数
{
  int sum = 0;
  for(int i = l; i <= r; i++)
  {
    sum = sum * 10 + num[i];
  }
  return sum;
}

int main()
{
  int n;
  cin >> n;
  while(next_permutation(num, num + 9)) //【全排列】
  {
    for(int i = 0;i < 9; i++) //枚举a的位数
    {
      int a = get(0, i);
      for(int j = i + 1; j < 9; j++) //枚举b与c的分界位数
      {
        int b = get(i + 1, j); 
        int c = get(j + 1, 8);
        if(n * c == a * c + b)
        {
          cnt ++;
        }
      }
    }
  }
  cout << cnt;
  return 0;
}

116. 飞行员兄弟

“飞行员兄弟”这个游戏,需要玩家顺利的打开一个拥有 16 个把手的冰箱。

已知每个把手可以处于以下两种状态之一:打开或关闭。

只有当所有把手都打开时,冰箱才会打开。

把手可以表示为一个 4×4 的矩阵,您可以改变任何一个位置 [i,j] 上把手的状态。

但是,这也会使得第 i 行和第 j 列上的所有把手的状态也随着改变。

请你求出打开冰箱所需的切换把手的次数最小值是多少。

输入格式
输入一共包含四行,每行包含四个把手的初始状态。

符号 + 表示把手处于闭合状态,而符号 - 表示把手处于打开状态。

至少一个手柄的初始状态是关闭的。

输出格式
第一行输出一个整数 N,表示所需的最小切换把手次数。

接下来 N 行描述切换顺序,每行输出两个整数,代表被切换状态的把手的行号和列号,数字之间用空格隔开。

注意:如果存在多种打开冰箱的方式,则按照优先级整体从上到下,同行从左到右打开。

数据范围
1≤i,j≤4
输入样例:

	-+--
	----
	----
	-+--

输出样例:
6
1 1
1 3
1 4
4 1
4 3
4 4

最优解:每个开关只用一次
此题按一次影响行列【十字型】 与费解的开关不同的是,每个灯泡会受到不同的开关的影响
最小字典序:矩阵0-15编号–>二进制枚举–>从小到大即满足
最一般情况【总结通用性公式】:难度究极版 : 208.开关问题【可高斯消元求解】

数组写法

#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;
 
const int N = 5;

char g[N][N], backup[N][N];//字符存储

int get(int x, int y)//矩阵位置坐标映射成[0, 15] --> 在二进制操作中为第几位
{
    return x * 4 + y;
}

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);//x行翻转
        turn_one(i, y);//y列翻转
    }

    turn_one(x, y);//交叉点被翻了两次抵消了, 需再翻一次
}

int main()
{
    for (int i = 0; i < 4; i ++ ) cin >> g[i];//读入字符串

    vector<PII> res;
    for (int op = 0; op < 1 << 16; op ++ )//枚举所有的操作, 每个位置按或不按(1或0) ==> 共2^16 (小于10^6)  的op 方案数   
    {
        vector<PII> temp;//临时存放每轮操作位置的坐标
        memcpy(backup, g, sizeof g);        // 备份初始状态

        // 进行操作
        for (int i = 0; i < 4; i ++ )
            for (int j = 0; j < 4; j ++ )
                if (op >> get(i, j) & 1)//映射开关的下标[0, 15]-->op的开关映射的位置&1-->算出每轮对应位状态按或不按[1或0]
                {
                    temp.push_back({i, j});//放入操作答案路径
                    turn_all(i, j);//对i, j进行十字型范围翻转操作
                }

        // 判断所有灯泡是否全亮
        bool has_closed = false;//标记
        for (int i = 0; i < 4; i ++ )
            for (int j = 0; j < 4; j ++ )
                if (g[i][j] == '+')
                    has_closed = true;//若遇到加号说明有关闭的, 没有全部打开

        if (has_closed == false)//说明全部关闭, 更新res , 属性min
        {
            if (res.empty() || res.size() > temp.size()) res = temp;//若为空说明还没有合法方案 或 有更优解更新
        }

        memcpy(g, backup, sizeof g);        // 还原成初始状态, 新一轮判断
    }

    cout << res.size() << endl;
    for (auto op : res) cout << op.x + 1 << ' ' << op.y + 1 << endl;

    return 0;
}

位运算写法-二进制优化


#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int,int> PII;
const int N = 4, INF = 100;

int change[N][N];

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

int main()
{
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j < N; j ++ )
        {
            for (int k = 0; k < N; k ++ ) change[i][j] += (1 << get(i, k)) + (1 << get(k, j));
            change[i][j] -= 1 << get(i, j);
        }

    int state = 0;
    for (int i = 0; i < N; i ++ )
    {
        string line;
        cin >> line;
        for (int j = 0; j < N; j ++ )
            if (line[j] == '+')
                state += 1 << get(i, j);
    }

    vector<PII> path;
    for (int i = 0; i < 1 << 16; i ++ )
    {
        int now = state;
        vector<PII> temp;
        for (int j = 0; j < 16; j ++ )
            if (i >> j & 1)
            {
                int x = j / 4, y = j % 4;
                now ^= change[x][y];
                temp.push_back({x, y});
            }
        if (!now && (path.empty() || path.size() > temp.size())) path = temp;
    }

    cout << path.size() << endl;
    for (auto &p : path)
        cout << p.first + 1 << ' ' << p.second + 1 << endl;

    return 0;
}

1208. 翻硬币

小明正在玩一个“翻硬币”的游戏。

桌上放着排成一排的若干硬币。我们用 * 表示正面,用 o 表示反面(是小写字母,不是零)。

比如,可能情形是:**oo***oooo

如果同时翻转左边的两个硬币,则变为:oooo***oooo

现在小明的问题是:如果已知了初始状态和要达到的目标状态,每次只能同时翻转相邻的两个硬币,那么对特定的局面,最少要翻动多少次呢?

我们约定:把翻动相邻的两个硬币叫做一步操作。

输入格式
两行等长的字符串,分别表示初始状态和要达到的目标状态。

输出格式
一个整数,表示最小操作步数

数据范围
输入字符串的长度均不超过100。
数据保证答案一定有解。

输入样例1:

**********
o****o****

输出样例1:
5
输入样例2:

*o**o***o***
*o***o**o***

输出样例2:
1

初始状态变化到目标状态 :常用BFS
仅限定一次翻转即为最优解

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n;
char start[N], aim[N]; //记录初始 -->目标状态  ,【不能用end 或 y : 库函数重名】


void turn(int i)
{
    if (start[i] == '*') start[i] = 'o';
    else start[i] = '*';
}

int main()
{
    cin >> start >> aim;//输入字符串 [一维char数组]
    n = strlen(start);//char数组长度

    int res = 0;
    for (int i = 0; i < n - 1; i ++ )//翻转到倒二个,包含倒一
        if (start[i] != aim[i])
        {
            turn(i), turn(i + 1);//依题意翻转
            res ++ ;//翻转次数
        }

    cout << res << endl;
    return 0;
}

2.1 二分与前缀和

789. 数的范围

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1 -1。

输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1。

数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
在这里插入图片描述

二分定位
首先判断能否使用二分:依据题目找到二分前提:升序排列区间

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

using namespace std;

const int N = 100010;

int n, m;
int q[N];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);  //没有&时 :Segmentation Fault   

    for (int i = 0; i < m; i ++ )
    {
        int x;
        scanf("%d", &x);
        // 二分x的左端点
        int l = 0, r = n - 1;   // 确定区间范围
        while (l < r)
        {
            int mid = l + r >> 1;
            if (q[mid] >= x) r = mid; //左->右,x在找到的区间的最大值[l = 0 , x] ,找大于等于x的第一个位置
            else l = mid + 1;
        }

        if (q[r] == x) //若找到左->右的x下标
        {
            cout << r << " "; 

            // 二分x的右端点
            r = n - 1;  // 右端点一定在[左端点, n - 1] 之间   
            while (l < r)
            {
                int mid = l + r + 1 >> 1;   // 因为写的是l = mid,所以需要补上1
                if (q[mid] <= x) l = mid;//右->左 x在找到的区间的最小值[x , r = n - 1] ,找小于等于x的第一个位置
                else r = mid - 1;
            }
            cout << r << endl;
        }
        else cout << "-1 -1" << endl;
    }

    return 0;
}

790. 数的三次方根

给定一个浮点数 n,求它的三次方根。

输入格式
共一行,包含一个浮点数 n。

输出格式
共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数。

数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000

浮点二分有细节,但easy

y = x 3 y=x^3 y=x3 具有有单调性可以二分,但不是只有单调性才可以二分。
[单调性为二分的充分不必要条件]

确定满足二分-->确定区间-->确定二段性(判断条件)

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
    double x;
    cin >> x;
    double l = -10000, r = 10000;//区间范围
    while (r - l > 1e-8)//误差精度范围 结果所在区间[l,r] < 1e-8    
    {
        double mid = (l + r) / 2;//double类型不能位运算 >> 1
        if (mid * mid * mid >= x) r = mid; //浮点数的二分好处,都是mid
        else l = mid;
    }

    printf("%lf\n", l);//double默认6位

    return 0;
}

795. 前缀和

输入一个长度为 n 的整数序列。

接下来再输入 m 个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

输入格式
第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。

输出格式
共 m 行,每行输出一个询问的结果。

数据范围
1≤l≤r≤n,
1≤n,m≤100000,
−1000≤数列中元素的值≤1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10

前缀和模板
注意下标用到i-1,s前缀和数组下标从1开始存放

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int a[N];   // 表示原数组
int s[N];   // 表示前缀和数组

int main()
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ )//用到下标i-1 ,下标从1开始
    {
        scanf("%d", &a[i]);
        s[i] = s[i - 1] + a[i];//前缀和数组
    }

    while (m -- )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l - 1]);
    }

    return 0;
}

796. 子矩阵的和

输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。

对于每个询问输出子矩阵中所有数的和。

输入格式
第一行包含三个整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式
共 q 行,每行输出一个询问的结果。

数据范围
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
在这里插入图片描述

二维前缀和:容斥原理
构造二维前缀和矩阵 : S x , y = S x − 1 , y + S x , y − 1 − S x − 1 , y − 1 + a x , y S_{x,y} = S_{x-1,y} + S_{x,y-1} - S_{x-1,y-1} + a_{x,y} Sx,y=Sx1,y+Sx,y1Sx1,y1+ax,y
用前缀和矩阵得到子矩阵的和 :给出(x1,y1)(x2,y2)求和: S x 2 , y 2 − S x 1 − 1 , y 2 − S x 2 , y 1 − 1 + S x − 1 , y − 1 S_{x2,y2} - S_{x1-1,y2} - S_{x2,y1-1} + S_{x-1,y-1} Sx2,y2Sx11,y2Sx2,y11+Sx1,y1


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

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], s[N][N];

int main()
{
    scanf("%d%d%d", &n, &m, &q);

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            scanf("%d", &a[i][j]);
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];//二维前缀和公式
        }

    while (q -- )
    {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);//子矩阵求和
    }

    return 0;
}

2.2 二分与前缀和——习题课

730. 机器人跳跃问题

机器人正在玩一个古老的基于 DOS 的游戏。

游戏中有 N+1 座建筑——从 0 到 N 编号,从左到右排列。

编号为 0 的建筑高度为 0 个单位,编号为 i 的建筑高度为 H(i) 个单位。

起初,机器人在编号为 0 的建筑处。

每一步,它跳到下一个(右边)建筑。

假设机器人在第 k 个建筑,且它现在的能量值是 E,下一步它将跳到第 k+1 个建筑。

如果 H(k+1)>E,那么机器人就失去 H(k+1)−E 的能量值,否则它将得到 E−H(k+1) 的能量值。

游戏目标是到达第 N 个建筑,在这个过程中能量值不能为负数个单位。

现在的问题是机器人至少以多少能量值开始游戏,才可以保证成功完成游戏?

输入格式
第一行输入整数 N。

第二行是 N 个空格分隔的整数,H(1),H(2),…,H(N) 代表建筑物的高度。

输出格式
输出一个整数,表示所需的最少单位的初始能量值上取整后的结果。

数据范围
1≤N,H(i)≤105,

输入样例1:
5
3 4 3 2 4
输出样例1:
4
输入样例2:
3
4 4 4
输出样例2:
4
输入样例3:
3
1 6 4
输出样例3:
3

发现规律+单调性可二分

规律: e’ = 2 * e - h
发现随着e增大 2 * e - h也增大 --> 具有单调性 —> 二分寻找答案

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

using namespace std;

const int N = 100010;

int n;
int h[N];

bool check(int e)//机器人是否合法通过
{
    for (int i = 1; i <= n; i ++ )
    {
        e = e * 2 - h[i];
        if (e < 0) return false;//能量e在过程不能小于0
        if (e >= 1e5) return true;//超过必定满足,只会越来越大 , 且不断增加会导致溢出, 需提前终止 
    }
    return true;
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);

    int l = 0, r = 1e5;//若e > 10^5  2 * e - h > 10^5 : 只需枚举[l,r] = [0,10^5]
    while (l < r)//O(logn)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }

    printf("%d\n", r);//也可以L

    return 0;
}

1221. 四平方和

四平方和定理,又称为拉格朗日定理:

每个正整数都可以表示为至多 4 个正整数的平方和。

如果把 0 包括进去,就正好可以表示为 4 个数的平方和。

比如:

5=02+02+12+22
7=12+12+12+22
对于一个给定的正整数,可能存在多种平方和的表示法。

要求你对 4 个数排序:

0≤a≤b≤c≤d
并对所有的可能表示法按 a,b,c,d 为联合主键升序排列,最后输出第一个表示法。

输入格式
输入一个正整数 N。

输出格式
输出4个非负整数,按从小到大排序,中间用空格分开。

数据范围
0<N< 5 ∗ 1 0 6 5∗10^6 5106
输入样例:
5
输出样例:
0 0 1 2


空间换时间

算法思想:
先枚举 c 2 + d 2 c^2 + d^2 c2+d2的所有值存入数组, 令sum = n - c 2 + d 2 c^2 + d^2 c2+d2, 枚举 a 2 + b 2 a^2 + b^2 a2+b2过程中判断是否出现在标记数组中
【是否C[sum](或D[sum])被标记】, 输出ans{a, b, C[sum], D[sum]};

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 5e6 + 10;

int n;

int C[N], D[N]; //存c与d的平方和为下标sum, c = C[sum],d = D[sum]  

int main()
{
    cin >> n;

    memset(C, -1, sizeof C);

    for (int c = 0; c * c <= n; c ++ )
        for (int d = c; c * c + d * d <= n; d ++ ){
            int sum = c * c + d * d;
            if (C[sum] == -1)
                C[sum] = c, D[sum] = d; //hash表<sum,对应组成c>  , <sum,对应组成d>
        }

    for (int a = 0; a * a <= n; a ++ )
        for (int b = a; a * a + b * b <= n; b ++ ){
            int sum = n - a * a - b * b;
            if (C[sum] != -1){
                printf("%d %d %d %d\n", a, b, C[sum], D[sum]);
                return 0;
            }
        }
    return 0;
}

二分 O ( N 2 l o g N ) O(N2logN) O(N2logN) 2721ms

#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const int N = 2500010;

struct Sum
{
    int s, c, d;
    bool operator< (const Sum &t)const
    {
        if (s != t.s) return s < t.s;
        if (c != t.c) return c < t.c;
        return d < t.d;
    }
}sum[N];

int n, m;

int main()
{
    cin >> n;

    for (int c = 0; c * c <= n; c ++ )
        for (int d = c; c * c + d * d <= n; d ++ )
            sum[m ++ ] = {c * c + d * d, c, d};//结构体按默认顺序赋值

    sort(sum, sum + m);//重载<

    for (int a = 0; a * a <= n; a ++ )
        for (int b = 0; a * a + b * b <= n; b ++ )
        {
            int t = n - a * a - b * b;
            int l = 0, r = m - 1;//范围
            while (l < r)//查找最后另一组平方和 a * a + b * b 
            {
                int mid = l + r >> 1;
                if (sum[mid].s >= t) r = mid;
                else l = mid + 1;
            }
            if (sum[l].s == t)//若存在输出答案
            {
                printf("%d %d %d %d\n", a, b, sum[l].c, sum[l].d);
                return 0;
            }
        }

    return 0;
}

1227. 分巧克力

儿童节那天有 K 位小朋友到小明家做客。

小明拿出了珍藏的巧克力招待小朋友们。

小明一共有 N 块巧克力,其中第 i 块是 Hi×Wi 的方格组成的长方形。

为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。

切出的巧克力需要满足:

形状是正方形,边长是整数
大小相同
例如一块 6×5 的巧克力可以切出 6 块 2×2 的巧克力或者 2 块 3×3 的巧克力。

当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行包含两个整数 Hi 和 Wi。

输入保证每位小朋友至少能获得一块 1×1 的巧克力。

输出格式
输出切出的正方形巧克力最大可能的边长。

数据范围
1≤N,K≤ 1 0 5 10^5 105,
1≤Hi,Wi≤ 1 0 5 10^5 105
输入样例:
2 10
6 5
5 6
输出样例:
2

单调性可二分

首先判断单调性【随着选取切除的边长越大切出的块数越少:单调递减==>可以二分】
边长x, 切出的块数res(需满足res >= k) 边长为x最多切的块数为: ⌊ w x ⌋ ∗ ⌊ h x ⌋ \lfloor \frac{w}{x} \rfloor * \lfloor \frac{h}{x} \rfloor xwxh (向下取整)


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

using namespace std;

const int N = 100010;

int n, k;
int h[N], w[N];

bool check(int mid) //判断边长mid能否切k块 
{
    int res = 0;//边长为mid时能切的块数res
    for (int i = 0; i < n; i ++ )//共n块巧克力来切
    {
        res += (h[i] / mid) * (w[i] / mid);// (向下取整) (长 / 划分的边长mid) * (宽 / 划分的边长mid)   [小学划分easy]
        if (res >= k) return true;//能切k块
    }

    return false;
}

int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &h[i], &w[i]);

    int l = 1, r = 1e5;//边长至少1 * 1 ,左边界L从1开始, 且右边界R不超过W或H (长宽) 
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;//边长为mid能切k块,往更大的尝试,找最大
        else r = mid - 1;//mid不满足,则在试试[1,mid - 1] 找满足的
    }

    printf("%d\n", r);

    return 0;
}

99. 激光炸弹

地图上有 N 个目标,用整数 Xi,Yi 表示目标在地图上的位置,每个目标都有一个价值 Wi。

注意:不同目标可能在同一位置。

现在有一种新型的激光炸弹,可以摧毁一个包含 R×R 个位置的正方形内的所有目标。

激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,y 轴平行。

求一颗炸弹最多能炸掉地图上总价值为多少的目标。

输入格式
第一行输入正整数 N 和 R,分别代表地图上的目标数目和正方形的边长,数据用空格隔开。

接下来 N 行,每行输入一组数据,每组数据包括三个整数 Xi,Yi,Wi,分别代表目标的 x 坐标,y 坐标和价值,数据用空格隔开。

输出格式
输出一个正整数,代表一颗炸弹最多能炸掉地图上目标的总价值数目。

数据范围
0≤R≤ 1 0 9 10^9 109
0<N≤10000,
0≤Xi,Yi≤5000
0≤Wi≤1000
输入样例:
2 1
0 0 1
1 1 1
输出样例:
1

在这里插入图片描述

前缀和下标从1开始
边长r : 能炸的目标数量 (r - 1) * (r - 1) : 价值用前缀和优化枚举[给的是坐标]

数据范围分析

0 < x,y < 5000 , 则R最多也就取5000 R 2 = 2.5 ∗ 1 0 7 < 1 0 8 R^2 = 2.5 * 10^7 < 10^8 R2=2.5107<108 可AC
2 * 2.5*1e7 / 1024 / 1024 = 200MB > 168MB(超过题目限制,不能开两个数组)

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 5010;

int n, m;
int s[N][N];

int main()
{
    int cnt, R;
    cin >> cnt >> R;
    R = min(5001, R);

    n = m = R;//枚举
    while (cnt -- )
    {
        int x, y, w;
        cin >> x >> y >> w;
        x ++, y ++ ;//编写细节最为重要, 前缀和用到i-1从1开始可以不用判断边界
        n = max(n, x), m = max(m, y);
        s[x][y] += w;
    }

    // 预处理前缀和
    for (int i = 1; i <= n; i ++ )//从1开始
        for (int j = 1; j <= m; j ++ )
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];

    int res = 0;

    // 枚举所有边长是R的矩形,枚举(i, j)为右下角
    for (int i = R; i <= n; i ++ )
        for (int j = R; j <= m; j ++ )
            res = max(res, s[i][j] - s[i - R][j] - s[i][j - R] + s[i - R][j - R]); //即(i-R+1,j-R+1) 到 (i,j)

    cout << res << endl;

    return 0;
}

1230. K倍区间

给定一个长度为 N 的数列,A1,A2,…AN,如果其中一段连续的子序列 Ai,Ai+1,…Aj 之和是 K 的倍数,我们就称这个区间 [i,j] 是 K 倍区间。

你能求出数列中总共有多少个 K 倍区间吗?

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行包含一个整数 Ai。

输出格式
输出一个整数,代表 K 倍区间的数目。

数据范围
1≤N,K≤100000,
1≤Ai≤100000
输入样例:
5 2
1
2
3
4
5
输出样例:
6


暴力双重枚举区间,看是不是k的倍数:【TLE】

前缀和优化 + 区间差值==k倍数(常用等效法) + 余数组合:

思路:
①以i为右端点的区间, (s[i] - s[i - 1] ~ s[i] - s[0] ) 有多少个区间值 % k == 0
②转化成s[i] - s[j] % k == 0 <==> s[j] % k == s[i] % k 余数是否相等【相等说明这一段的数的和是k的倍数】

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

using namespace std;

typedef long long LL;

const int N = 100010;

int n, k;//从n个数中找区间为k的倍数
LL s[N], cnt[N];// s[N]最大10^10    cnt数组:cnt[x]:余数为x的数有多少

int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i ++ )//构造前缀和
    {
        scanf("%lld", &s[i]);
        s[i] += s[i - 1];
    }

    LL res = 0;
    cnt[0] = 1;//一个都不选,则余数为0,初始有1个 
    for (int i = 1; i <= n; i ++ )//枚举右端点
    {   //res一开始是表示的相减满足题目的条件,因为一旦再出现%k和res数组里面有相当的情况,就全加一遍(和前面全部组合一遍)
        res += cnt[s[i] % k];//加上新增的组合[2个相同就有1个k倍区间]
        cnt[s[i] % k] ++ ;//统计 前缀和 % k 余数个数
    }

    printf("%lld\n", res);

    return 0;
}

3.1 数学与简单DP

1205. 买不到的数目

小明开了一家糖果店。

他别出心裁:把水果糖包成4颗一包和7颗一包的两种。

糖果不能拆包卖。

小朋友来买糖的时候,他就用这两种包装来组合。

当然有些糖果数目是无法组合出来的,比如要买 10 颗糖。

你可以用计算机测试一下,在这种包装情况下,最大不能买到的数量是17。

大于17的任何数字都可以用4和7组合出来。

本题的要求就是在已知两个包装的数量时,求最大不能组合出的数字。

输入格式
两个正整数 n,m,表示每种包装中糖的颗数。

输出格式
一个正整数,表示最大不能买到的糖数。

数据范围
2≤n,m≤1000,
保证数据一定有解。

输入样例:
4 7
输出样例:
17

数论公式
小学数二结论题(证明略):若两个整数p、q互质 ,则p,q不能凑出的最小整数为 (p - 1)*(q - 1) - 1

#include <iostream>

using namespace std;

int n,m;

int main()
{
    cin >> n >> m;
    cout << (n - 1) * (m - 1) - 1 << endl;
    return 0;
}

1211. 蚂蚁感冒

在这里插入图片描述
在这里插入图片描述

转换思路:掉头等价于穿过
a[0] > 0感冒的左边:向左不会碰(先走出去),向右不会碰(感冒先走出去) , 感冒的右边:向左会碰,向右不会碰(先走出去)
再结合相遇掉头:感冒右边的向左会碰与向右不会碰还要分类【如果左边有则会掉头,则右边会被感染】
a[0] < 0感冒的左边:向左不会碰(先走出去),向右会碰 , 感冒的右边:向左不会碰(感冒先走出去),向右不会碰(先走出去)
再结合相遇掉头:感冒左边的向右会碰与向右不会碰还要分类【如果右边有则会掉头,则左边会被感染】

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 55;

int n;
int a[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];

    int left = 0, right = 0;    // 分别表示左边向右走的蚂蚁数量,和右边向左走的蚂蚁数量
    for (int i = 1; i < n; i ++ )
        if (abs(a[i]) < abs(a[0]) && a[i] > 0) left ++ ;
        else if (abs(a[i]) > abs(a[0]) && a[i] < 0) right ++ ;

    if (a[0] > 0 && right == 0 || a[0] < 0 && left == 0) cout << 1 << endl;//这种情况仅自己,不会碰到别的蚂蚁
    else cout << left + right + 1 << endl;//左边有则回头时右边也会碰,再加上自己

    return 0;
}

1216. 饮料换购

乐羊羊饮料厂正在举办一次促销优惠活动。乐羊羊C型饮料,凭3个瓶盖可以再换一瓶C型饮料,并且可以一直循环下去(但不允许暂借或赊账)。

请你计算一下,如果小明不浪费瓶盖,尽量地参加活动,那么,对于他初始买入的 n 瓶饮料,最后他一共能喝到多少瓶饮料。

输入格式
输入一个整数 n,表示初始买入的饮料数量。

输出格式
输出一个整数,表示一共能够喝到的饮料数量。

数据范围
0<n<10000
输入样例:
100
输出样例:
149

简单模拟

#include<iostream>

using namespace std;

int n,res;

int main()
{
    cin >> n;
    res += n;
    while(n / 3)//y总 : n >= 3
    {
        res += n / 3;
        n = n / 3 + n % 3; 
    }

    cout << res << endl;

    return 0;
}

2. 01背包【详细版】

从1开始存放
朴素


#include <iostream>
using namespace std; 
#include<algorithm>
const int N = 1010;
int v[N],w[N];//体积,价值
int dp[N];
int f[N][N]; //爆栈会直接导致无法编译 , 各种问题 
int n,m;

int main()
{
	cin >> n >> m ;
	
	for(int i = 1;i <= n;i++) cin >>  v[i] >> w[i]; 
	
	for(int i = 1;i <= n;i++)
		for(int j = v[i];j <= m;j++) //0~v[i]之前放不下,没有意义
		{
			f[i][j] = f[i-1][j]; //左边状态			
			if(j >= v[i]) f[i][j] = max(f[i][j] , f[i-1][j - v[i]] + w[i]);	 //右边状态 
		}//【j-1中选出的最大值+选了i后+w[i]即】	
	cout << f[n][m] << endl; 
	
	return 0;
}

一维优化

#include <iostream>
using namespace std; 
#include<algorithm>
const int N = 1010;
int n,m; //种类n , 体积m
int v[N],w[N];//体积,价值
int dp[N];


//【由去掉一维,发现状态==i-1,不对,则j从m开始到v[i],j-v[i]还没有更新,等效i-1的,可降成一维】
//【可优化原因】:分两种讨论用的j和j-v[i] <= j且i的用f(i-1)状态,滚动
//转化成一维: - 滚动数组 
int main() 
{
	cin >> n >> m ;
	
	for(int i = 1;i <= n;i++) cin >>  v[i] >> w[i];
	
	for(int i = 1;i <= n;i++)
		for(int j = m;j >= v[i];j--)   //若正的取变成 f[i][j - v[i]] + w[i]  而不是i-1
			dp[j] = max(dp[j] , dp[j - v[i]] +w[i]);
			
	cout << dp[m] << endl;  //体积取m时,最大价值dp[m]
		
	return 0;
}

边输出边处理

#include <iostream>
using namespace std; 
#include<algorithm>
const int N = 1010;
int n,m; //种类n , 体积m
int v[N],w[N];//体积,价值
int dp[N];


//【由去掉一维,发现状态==i-1,不对,则j从m开始到v[i],j-v[i]还没有更新,等效i-1的,可降成一维】
//【可优化原因】:分两种讨论用的j和j-v[i] <= j且i的用f(i-1)状态,滚动
//转化成一维: - 滚动数组 
int main() 
{
	cin >> n >> m ;
	
	for(int i = 1;i <= n;i++) cin >>  v[i] >> w[i];
	
	for(int i = 1;i <= n;i++)
		for(int j = m;j >= v[i];j--)   //若正的取变成 f[i][j - v[i]] + w[i]  而不是i-1
			dp[j] = max(dp[j] , dp[j - v[i]] +w[i]);
			
	cout << dp[m] << endl;  //体积取m时,最大价值dp[m]
		
	return 0;
}

1015. 摘花生

Hello Kitty想摘点花生送给她喜欢的米老鼠。

她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。

地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。

Hello Kitty只能向东或向南走,不能向西或向北走。

问Hello Kitty最多能够摘到多少颗花生。

在这里插入图片描述

输入格式
第一行是一个整数T,代表一共有多少组数据。

接下来是T组数据。

每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C。

每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有C个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目M。

输出格式
对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数。

数据范围
1≤T≤100,
1≤R,C≤100,
0≤M≤1000
输入样例:
2
2 2
1 1
3 4
2 3
2 3 4
1 6 5
输出样例:
8
16
在这里插入图片描述

#include<cstdio>
#include<algorithm>

using  namespace std;

const int N = 110;

int w[N][N];
int f[N][N];

int main()
{
    int T;
    scanf("%d", &T);
    int n, m;
    while(T --)
    {
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++) //用到f[i-1][j]   f[i][j-1]
            for(int j = 1; j <= m; j++)
                scanf("%d", &w[i][j]);

        for(int i = 1; i <= n; i++)
                for(int j = 1; j <= m; j++)
                    f[i][j] = max(f[i - 1][j], f[i][j - 1]) + w[i][j];

        printf("%d\n", f[n][m]);
    }
    return 0;
}

895. 最长上升子序列

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1≤N≤1000,
−109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
经典DP:最长上升子序列
f[i]表示从第一个数字开始算,以a[i]结尾的最大的上升序列


#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n;
int a[N];
int f[N];

int main()
{
    cin >> n ;
    for (int i = 1; i <= n; i ++ ) cin >> a[i];

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
        f[i] = 1;
        for (int j = 1; j < i; j ++ )
            if (a[i] > a[j])
                f[i] = max(f[i], f[j] + 1);

        res = max(res, f[i]);
    }

    cout << res << endl;

    return 0;
}

3.2 数学与简单DP——习题课

1212. 地宫取宝

X 国王有一个地宫宝库,是 n×m 个格子的矩阵,每个格子放一件宝贝,每个宝贝贴着价值标签。

地宫的入口在左上角,出口在右下角。

小明被带到地宫的入口,国王要求他只能向右或向下行走。

走过某个格子时,如果那个格子中的宝贝价值比小明手中任意宝贝价值都大,小明就可以拿起它(当然,也可以不拿)。

当小明走到出口时,如果他手中的宝贝恰好是 k 件,则这些宝贝就可以送给小明。

请你帮小明算一算,在给定的局面下,他有多少种不同的行动方案能获得这 k 件宝贝。

输入格式
第一行 3 个整数,n,m,k,含义见题目描述。

接下来 n 行,每行有 m 个整数 Ci 用来描述宝库矩阵每个格子的宝贝价值。

输出格式
输出一个整数,表示正好取 k 个宝贝的行动方案数。

该数字可能很大,输出它对 1000000007 取模的结果。

数据范围
1≤n,m≤50,
1≤k≤12,
0≤Ci≤12
输入样例1:
2 2 2
1 2
2 1
输出样例1:
2
输入样例2:
2 3 2
1 2 3
2 1 5
输出样例2:
14
在这里插入图片描述

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 55, MOD = 1000000007;

int n, m, k;
int w[N][N];
int f[N][N][13][14];

int main()
{
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            cin >> w[i][j];
            w[i][j] ++ ;
        }

    f[1][1][1][w[1][1]] = 1;
    f[1][1][0][0] = 1;

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            if (i == 1 && j == 1) continue;
            for (int u = 0; u <= k; u ++ )
                for (int v = 0; v <= 13; v ++ )
                {
                    int &val = f[i][j][u][v];
                    val = (val + f[i - 1][j][u][v]) % MOD;
                    val = (val + f[i][j - 1][u][v]) % MOD;
                    if (u > 0 && v == w[i][j])
                    {
                        for (int c = 0; c < v; c ++ )
                        {
                            val = (val + f[i - 1][j][u - 1][c]) % MOD;
                            val = (val + f[i][j - 1][u - 1][c]) % MOD;
                        }
                    }
                }
        }

    int res = 0;
    for (int i = 0; i <= 13; i ++ ) res = (res + f[n][m][k][i]) % MOD;

    cout << res << endl;

    return 0;
}

1214. 波动数列

观察这个数列:

1 3 0 2 -1 1 -2 …

这个数列中后一项总是比前一项增加2或者减少3,且每一项都为整数。

栋栋对这种数列很好奇,他想知道长度为 n 和为 s 而且后一项总是比前一项增加 a 或者减少 b 的整数数列可能有多少种呢?

输入格式
共一行,包含四个整数 n,s,a,b,含义如前面所述。

输出格式
共一行,包含一个整数,表示满足条件的方案数。

由于这个数很大,请输出方案数除以 100000007 的余数。

数据范围
1≤n≤1000,
− 1 0 9 −10^9 109≤s≤ 1 0 9 10^9 109,
1≤a,b≤ 1 0 6 10^6 106
输入样例:
4 10 2 3
输出样例:
2
样例解释
两个满足条件的数列分别是2 4 1 3和7 4 1 -2。

4.1 枚举、模拟与排序


九字型从中间枚举i与j均[-1,1] 在这里插入图片描述


1210. 连号区间数

小明这些天一直在思考这样一个奇怪而有趣的问题:

在 1∼N 的某个排列中有多少个连号区间呢?

这里所说的连号区间的定义是:

如果区间 [L,R] 里的所有元素(即此排列的第 L 个到第 R 个元素)递增排序后能得到一个长度为 R−L+1 的“连续”数列,则称这个区间连号区间。

当 N 很小的时候,小明可以很快地算出答案,但是当 N 变大的时候,问题就不是那么简单了,现在小明需要你的帮助。

输入格式
第一行是一个正整数 N,表示排列的规模。

第二行是 N 个不同的数字 Pi,表示这 N 个数字的某一排列。

输出格式
输出一个整数,表示不同连号区间的数目。

数据范围
1≤N≤10000,
1≤Pi≤N
输入样例1:
4
3 2 4 1
输出样例1:
7
输入样例2:
5
3 4 2 5 1
输出样例2:
9
样例解释
第一个用例中,有 7 个连号区间分别是:[1,1],[1,2],[1,3],[1,4],[2,2],[3,3],[4,4]
第二个用例中,有 9 个连号区间分别是:[1,1],[1,2],[1,3],[1,4],[1,5],[2,2],[3,3],[4,4],[5,5]

暴力做法 for i for j 再sort for k to j - i + 1 判断

1-N排列隐藏性质:没有重复的数
得出区间[a,b]最大值与最小值等于排序好的最大值最小值 :
if(maxv - minv == b - a)说明全部填满,则[a,b]为连号区间
即满足上述性质为连号区间,初始 max = -INF , min = INF保证更新
每次加入新的数比较大小(优化成两重循环)


#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010, INF = 1e8;//取大于边界范围就行

int n;
int a[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];

    int res = 0;
    for (int i = 0; i < n; i ++ )   // 枚举区间左端点
    {
        int minv = INF, maxv = -INF;
        for (int j = i; j < n; j ++ )   // 枚举区间右端点
        {
            minv = min(minv, a[j]);
            maxv = max(maxv, a[j]);
            if (maxv - minv == j - i) res ++ ;//说明sort(a[i~j]) -->minv = a[i] maxv = a[j] 
        }
    }

    cout << res << endl;

    return 0;
}

1236. 递增三元组

给定三个整数数组

A=[A1,A2,…AN],
B=[B1,B2,…BN],
C=[C1,C2,…CN],

请你统计有多少个三元组 (i,j,k) 满足:

1≤i,j,k≤N
Ai<Bj<Ck
输入格式
第一行包含一个整数 N。

第二行包含 N 个整数 A1,A2,…AN。

第三行包含 N 个整数 B1,B2,…BN。

第四行包含 N 个整数 C1,C2,…CN。

输出格式
一个整数表示答案。

数据范围
1≤N≤105,
0≤Ai,Bi,Ci≤105
输入样例:
3
1 1 1
2 2 2
3 3 3
输出样例:
27


2023-简版STL二分
∑   ( \sum~(  (a[]中第一个大等于b[i]的位置 × \times × c[]中第一个小于b的位置 ) ) )

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N], b[N], c[N];

int main() 
{
	int n;
	scanf("%d", &n);
	for(int i = 0; i < n; i++) scanf("%d", &a[i]);
	for(int i = 0; i < n; i++) scanf("%d", &b[i]);
	for(int i = 0; i < n; i++) scanf("%d", &c[i]);
	sort(a, a + n), sort(b, b + n), sort(c, c + n);
	LL cnt = 0;
	for(int i = 0; i < n; i++)  //b比a大 且 比c小 - 【枚举b[]  乘积 
	{
		LL A = lower_bound(a, a + n, b[i]) - a; //a[]中第一个大等于b[i]的位置 (从0开始刚好 个数 == 下标) 
		LL C = n - (upper_bound(c, c + n, b[i]) - c); //c[]中第一个小于b的位置 (剩下均大于b[i]) 
		cnt += A * C;
	}
	printf("%lld", cnt);
	return 0;
}

三指针

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N], b[N], c[N];

int main() 
{
	int n;
	scanf("%d", &n);
	for(int i = 0; i < n; i++) scanf("%d", &a[i]);
	for(int i = 0; i < n; i++) scanf("%d", &b[i]);
	for(int i = 0; i < n; i++) scanf("%d", &c[i]);
	sort(a, a + n), sort(b, b + n), sort(c, c + n);
	LL sum = 0,s1 = 0,s2 = 0;
    for(int i=0;i<n;i++){
        while(s1 < n && a[s1] < b[i]) s1++;
            while(s2 < n && c[s2] <= b[i]) s2++;
        sum += s1 * (n - s2);
    }
	printf("%lld", sum);
	return 0;
}

before

暴力:for三重 if(a[i] < b[i] && b[i] < c[i])res++;

通过时间复杂度需限制再O(nlogn) 则只能枚举一个数组 ,应枚举B[i],因为A[i]与C[i]只需被B[i]限制
再把A、C中满足的个数相乘 [等效满足的A<B<C的组合]

实现:O(n)法①:前缀和 O(nlogn)法②:sort(A) + 二分法(B[j])找到A中小于B[j]的下标res :答案个数就为 res + 1
前缀和映射hash有减1操作,但有数值0的,所以每位加1,取值映射变为[1,1e5 + 1],(相对大小不变)
把数值映射为hash值 , 前缀和数组存储值小于当前下标的个数,同理c就存储大于当前下标的个数【类似桶排序】

佬滴总结:

既然要排序实现快速的查找A中小于B[i]的数的个数,可以将数组A中所有元素出现的次数存入一个哈希表中,因为数组中元素的范围只有 n 5 n^5 n5, 可以开一个大的数组cnt作为哈希表。
在枚举B中元素时,我们需要快速查找找小于B[i]的所有元素的总数,只需要在枚举之前先将求出表中各数的前缀和即可。

前缀和实现

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

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int a[N], b[N], c[N];
int as[N];  // as[i]表示在A[]中有多少个数小于b[i]
int cs[N];  // cs[i]表示在C[]中有多少个数大于b[i]
int cnt[N], s[N];//cnt[]存a的值的数量

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]), a[i] ++ ;
    for (int i = 0; i < n; i ++ ) scanf("%d", &b[i]), b[i] ++ ;
    for (int i = 0; i < n; i ++ ) scanf("%d", &c[i]), c[i] ++ ;

    // 求as[]
    for (int i = 0; i < n; i ++ ) cnt[a[i]] ++ ;//将数组a中所有元素出现的次数存入一个哈希表中
    for (int i = 1; i < N; i ++ ) s[i] = s[i - 1] + cnt[i];   // 求cnt[]的前缀和
    for (int i = 0; i < n; i ++ ) as[i] = s[b[i] - 1];//a[]中小于b[i]的元素个数前缀和 :前缀和s可表示为不超过下标值的元素个数(所以减1)

    // 求cs[]
    memset(cnt, 0, sizeof cnt);
    memset(s, 0, sizeof s);
    for (int i = 0; i < n; i ++ ) cnt[c[i]] ++ ;//将数组c中所有元素出现的次数存入一个哈希表中
    for (int i = 1; i < N; i ++ ) s[i] = s[i - 1] + cnt[i];
    for (int i = 0; i < n; i ++ ) cs[i] = s[N - 1] - s[b[i]]; //s[N-1] - s[b[i]]表示:c[]所有元素中大于b[i]的个数

    // 枚举每个b[i]
    LL res = 0;
    for (int i = 0; i < n; i ++ ) res += (LL)as[i] * cs[i];//1e5*1e5 会爆int 开LL

    cout << res << endl;

    return 0;
}

1245. 特别数的和

小明对数位中含有 2、0、1、9 的数字很感兴趣(不包括前导 0),在 1 到 40 中这样的数包括 1、2、9、10 至 32、39 和 40,共 28 个,他们的和是 574。

请问,在 1 到 n 中,所有这样的数的和是多少?

输入格式
共一行,包含一个整数 n。

输出格式
共一行,包含一个整数,表示满足条件的数的和。

数据范围
1≤n≤10000
输入样例:
40
输出样例:
574

简单模拟

#include <iostream>
#include <algorithm>

using namespace std;

int n;
int res;

bool check(int i)
{
    while(i)
    {
        int t = i % 10;//t取个位的值
        i /= 10;//删除个位
        if(t == 1 || t == 2 || t == 9 || t == 0 ) return true;
        
    }
    return false;
}

int main()
{
    cin >> n;
    for(int i = 1;i <= n;i++)
    {
        if(check(i)) res += i;
    }
    
    cout << res << endl;
    
    return 0;
}

#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
    int n;
    cin >> n;

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int x = i;
        while (x)
        {
            int t = x % 10; // 取出x的个位
            x /= 10;    // 删掉x的个位
            if (t == 2 || t == 0 || t == 1 || t == 9)
            {
                res += i;
                break;
            }
        }
    }

    cout << res << endl;

    return 0;
}

扩:数据如果很大,要用到数位dp

1204. 错误票据

某涉密单位下发了某种票据,并要在年终全部收回。

每张票据有唯一的ID号。

全年所有票据的ID号是连续的,但ID的开始数码是随机选定的。

因为工作人员疏忽,在录入ID号的时候发生了一处错误,造成了某个ID断号,另外一个ID重号。

你的任务是通过编程,找出断号的ID和重号的ID。

假设断号不可能发生在最大和最小号。

输入格式
第一行包含整数 N,表示后面共有 N 行数据。

接下来 N 行,每行包含空格分开的若干个(不大于100个)正整数(不大于100000),每个整数代表一个ID号。

输出格式
要求程序输出1行,含两个整数 m,n,用空格分隔。

其中,m表示断号ID,n表示重号ID。

数据范围
1≤N≤100
输入样例:
2
5 6 8 11 9
10 12 9
输出样例:
7 9

sstream 用法(严格读取每行,不熟做题就不用hh) 【都是自己试试就知道默认规则,然后记住】

知识点:getline() 读取回车,占空位
scanf读到文件结束符时会返回-1,而不是0。

stringstream ssin版

#include <cstring>
#include <sstream>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010;

int n;
int a[N];

int main()
{
    int cnt;
    cin >> cnt;
    string line;

    getline(cin, line); // 忽略掉第一行的回车
    while (cnt -- )
    {
        getline(cin, line);
        stringstream ssin(line);//stringstream类型的ssin代替cin实现读入

        while (ssin >> a[n]) n ++ ;
    }

    sort(a, a + n);

    int res1, res2;
    for (int i = 1; i < n; i ++ )
        if (a[i] == a[i - 1]) res2 = a[i];  // 重号
        else if (a[i] >= a[i - 1] + 2) res1 = a[i] - 1; // 断号

    cout << res1 << ' ' << res2 << endl;

    return 0;
}

此题简单读法版: 读完n忽略,后面的读到空为止

#include <iostream>
#include <algorithm>
#include <sstream>

using namespace std;
const int N = 1e4 + 5;
int n, cnt;
int a[N];

int main () {
    cin >> n;
    int t;
    while (cin >> t) {
        a[cnt ++] = t;
    }
    sort (a, a + cnt);
    int res1 = 0, res2 = 0;
    for (int i = 1; i < cnt; i ++) {
        if (res1 && res2)
            break;
        if (a[i] - a[i - 1] == 2)
            res1 = a[i - 1] + 1;
        if (a[i] == a[i - 1])
            res2 = a[i - 1];
    }
    cout << res1 << ' ' << res2;

}

找到区间[min,max]读取版(类似连续区间规律)

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5+5, INF = 0x3f3f3f3f;

int ha[N];

int main()
{
    int n;
    cin >> n;//这个读入没什么用
    int minv = INF, maxv = -INF;
    int tp;
    while(cin >> tp)//直接读到文件尾部停止
    {
        if(tp < minv)   minv = tp;
        if(tp > maxv)   maxv = tp;
        ha[tp] ++;
    }
    int ans1 = 0, ans2 = 0;
    for(int i = minv; i <= maxv; ++ i)
    {
        if(ha[i] == 0)  ans1 = i;
        if(ha[i] == 2)  ans2 = i;
    }
    cout << ans1 << " " << ans2 << endl;
}

//作者:chaosliang  链接:https://www.acwing.com/solution/content/31989/
这份代码是多年前刚学c++写的,有些不足之处(这样写导致line是没有用的 但这题对答案是没有影响的
如果要严格按照每行读取数据可以采用getline(cin,s)的方法先读取整行
然后用sstream的这种方式来分割,再转成数字(或者遍历字符串手动分割)

下面是sstream的简易用法

//Example:可以用于分割被空格、制表符等符号分割的字符串
#include<iostream>  
#include<sstream>        //istringstream 必须包含这个头文件
#include<string>  
using namespace std;  
int main(){  
    string str="i am a boy";  
    istringstream is(str);  
    string s;  
    while(is>>s)  {  
        cout<<s<<endl;  
    }  
} 


//作者:熠丶 链接:https://www.acwing.com/solution/content/7614/

466. 回文日期

在日常生活中,通过年、月、日这三个要素可以表示出一个唯一确定的日期。

牛牛习惯用 8 位数字表示一个日期,其中,前 4 位代表年份,接下来 2 位代表月份,最后 2 位代表日期。

显然:一个日期只有一种表示方法,而两个不同的日期的表示方法不会相同。

牛牛认为,一个日期是回文的,当且仅当表示这个日期的 8 位数字是回文的。

现在,牛牛想知道:在他指定的两个日期之间(包含这两个日期本身),有多少个真实存在的日期是回文的。

一个 8 位数字是回文的,当且仅当对于所有的 i(1≤i≤8) 从左向右数的第 i 个数字和第 9−i 个数字(即从右向左数的第 i 个数字)是相同的。

例如:

对于 2016 年 11 月 19 日,用 8 位数字 20161119 表示,它不是回文的。
对于 2010 年 1 月 2 日,用 8 位数字 20100102 表示,它是回文的。
对于 2010 年 10 月 2 日,用 8 位数字 20101002 表示,它不是回文的。
输入格式
输入包括两行,每行包括一个 8 位数字。

第一行表示牛牛指定的起始日期 date1,第二行表示牛牛指定的终止日期 date2。保证 date1 和 date2 都是真实存在的日期,且年份部分一定为 4 位数字,且首位数字不为 0。

保证 date1 一定不晚于 date2。

输出格式
输出共一行,包含一个整数,表示在 date1 和 date2 之间,有多少个日期是回文的。

输入样例:
20110101
20111231
输出样例:
1

(枚举,模拟) O ( 1 0 4 ) O(10^4) O(104)
y总:由于只有八位数,而且回文串左右对称,因此可以只枚举左半边,这样只需枚举 0∼9999总共一万个数,然后判断:
①枚举回文串
②是否在给定范围[date1,date2]内
③日期是否合法;
时间复杂度:一共枚举 1 0 4 10^4 104 个数,判断每个数是否合法的计算量是常数级别的,因此总计算量是 O ( 1 0 4 ) O(10^4) O(104)

【想法】
% 1 0 n 10^n 10n : 取末尾n位
/ 1 0 n 10^n 10n : 删除末尾n位

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

using namespace std;

int months[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

bool check(int date)//检查年月日是否合法
{
    int year = date / 10000;
    int month = date % 10000 / 100;//%10000等效取后四位 ,/100删去后两位  
    int day = date % 100;

    if (!month || month >= 13 || !day) return false;

    if (month != 2 && day > months[month]) return false;//先不管二月
    if (month == 2)//特判二月
    {
        bool leap = year % 4 == 0 && year % 100 || year % 400 == 0;//是否闰年
        if (day > 28 + leap) return false;
    }

    return true;
}

int main()
{
    int date1, date2;
    cin >> date1 >> date2;

    int res = 0;
    for (int i = 0; i < 10000; i ++ )
    {
        int x = i, r = i;
        for (int j = 0; j < 4; j ++ ) r = r * 10 + x % 10, x /= 10;//拼接,取最后一位加上 + 原数值乘10 如:1234 --> 12344321 

        if (r >= date1 && r <= date2 && check(r)) res ++ ;//检查是否在区间[date1,date2]内
    }

    printf("%d\n", res);
    return 0;
}

787. 归并排序

给定你一个长度为 n 的整数数列。

请你使用归并排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式
输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在 1∼109 范围内),表示整个数列。

输出格式
输出共一行,包含 n 个整数,表示排好序的数列。

数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5

熟能生巧

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int q[N];
int tmp[N];

void merge_sort(int l , int r)
{
    if(l >= r)return;
    int mid = l + r >> 1;
    merge_sort(l , mid ) , merge_sort(mid + 1 , r);
    int k = 0 ,i = l  ,j = mid + 1;
    while(i <= mid && j <= r)
        if(q[i] <= q[j])tmp[k++] = q[i++];
        else tmp[k++] = q[j++];

    while(i <= mid) tmp[k++] = q[i++];
    while(j <= r) tmp[k++] = q[j++];

    for(int i = l , j  = 0;j < k; i ++ , j ++) q[i] = tmp[j];

}

int main()
{
    cin >> n;
    for(int i  = 0;i < n;i++) scanf("%d", &q[i]);
    merge_sort(0,n - 1);
    for(int i = 0;i < n;i++) printf("%d ",q[i]);

    return 0;
}

4.2 枚举、模拟与排序——习题课

1219. 移动距离

X星球居民小区的楼房全是一样的,并且按矩阵样式排列。

其楼房的编号为 1,2,3…
当排满一行时,从下一行相邻的楼往反方向排号。

比如:当小区排号宽度为 6 时,开始情形如下:

1 2 3 4 5 6
12 11 10 9 8 7
13 14 15 …
我们的问题是:已知了两个楼号 m 和 n,需要求出它们之间的最短移动距离(不能斜线方向移动)。

输入格式
输入共一行,包含三个整数 w,m,n,w 为排号宽度,m,n 为待计算的楼号。

输出格式
输出一个整数,表示 m,n 两楼间最短移动距离。

数据范围
1≤w,m,n≤10000,

输入样例:
6 8 2
输出样例:
4
在这里插入图片描述

找规律 : 奇数行-偶数行
给t    矩阵  [1,t] , [t+1,2t] ...  [(i-1)*t + 1, i*t]
abs(x1 - x2)  + abs(y1 - y2)
分类 :奇数行 / 偶数行 

找规律 : 先变成从0开始好做 此时行号:x1 = n / w  , 列号: y1 = n % w    
从0开始时:(第一行行号为0)  奇数行号翻转从右往左, 偶数行正常从左往右

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
    int w, m, n;
    cin >> w >> m >> n;
    m --, n -- ;//先下标-- ; 从0开始,行号列号规律就简单了[不用特判]

    int x1 = m / w, x2 = n / w;
    int y1 = m % w, y2 = n % w;
    if (x1 % 2) y1 = w - 1 - y1;//尝试带入即可得出规律   
    if (x2 % 2) y2 = w - 1 - y2;

    cout << abs(x1 - x2) + abs(y1 - y2) << endl; //曼哈顿距离, 但此题没有用其性质找规律

    return 0;
}

1229. 日期问题

小明正在整理一批历史文献。这些历史文献中出现了很多日期。

小明知道这些日期都在1960年1月1日至2059年12月31日。

令小明头疼的是,这些日期采用的格式非常不统一,有采用年/月/日的,有采用月/日/年的,还有采用日/月/年的。

更加麻烦的是,年份也都省略了前两位,使得文献上的一个日期,存在很多可能的日期与其对应。

比如02/03/04,可能是2002年03月04日、2004年02月03日或2004年03月02日。

给出一个文献上的日期,你能帮助小明判断有哪些可能的日期对其对应吗?

输入格式
一个日期,格式是”AA/BB/CC”。

即每个’/’隔开的部分由两个 0-9 之间的数字(不一定相同)组成。

输出格式
输出若干个不相同的日期,每个日期一行,格式是”yyyy-MM-dd”。

多个日期按从早到晚排列。

数据范围
0≤A,B,C≤9
输入样例:
02/03/04
输出样例:
2002-03-04
2004-02-03
2004-03-02


①日期合法判断 check_date(y, m, d)
②匹配判断 success(y, m, d)

可根据数据范围,空间范围选择char还是int
特殊读入处理:scanf(“%d/%d/%d”, &a, &b, &c);

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

using namespace std;

int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

bool check_valid(int year, int month, int day)//判断日期是否合法
{
    if (month == 0 || month > 12) return false;
    if (day == 0) return false;
    if (month != 2)
    {
        if (day > days[month]) return false;
    }
    else
    {
        int leap = year % 100 && year % 4 == 0 || year % 400 == 0;
        if (day > 28 + leap) return false;
    }

    return true;
}

int main()
{
    int a, b, c;
    scanf("%d/%d/%d", &a, &b, &c);//【还能这么读!】

    for (int date = 19600101; date <= 20591231; date ++ )//暴力枚举
    {
        int year = date / 10000, month = date % 10000 / 100, day = date % 100;//取年月日
        if (check_valid(year, month, day))
        {
            if (year % 100 == a && month == b && day == c ||        // 年/月/日
                month == a && day == b && year % 100 == c ||        // 月/日/年
                day == a && month == b &&year % 100 == c)           // 日/月/年
                printf("%d-%02d-%02d\n", year, month, day);
        }
    }

    return 0;
}


//直接判断  【因为答案最多就三种判断每种情况是否合法且在范围内】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int month[13]={0,31,28,31,30,31,30,31,31,30,31,30,31};
struct node
{
    int yy,mm,dd;
}s[5];
int cnt=0;
void get(int yy,int mm,int dd)
{
    if(yy>=60)yy=1900+yy;
    else yy=2000+yy;
    // error:day等于00的情况没考虑
    if(mm==0||mm>12||dd==0)return;
    if(mm!=2)
    {
        if(dd>month[mm])return;
    }
    else
    {
        int flag=(yy%4==0&&yy%100)||(yy%400==0);
        if(dd>28+flag)return;
    }
    s[cnt++]={yy,mm,dd};
}
int cmp(node & a,node & b)
{
    return a.yy==b.yy?(a.mm==b.mm?a.dd<b.dd:a.mm<b.mm):a.yy<b.yy;
}
int main()
{
    int yy,mm,dd;
    // 不能使用cin读入
    scanf("%d/%d/%d", &yy, &mm,&dd);
    get(yy,mm,dd);
    get(dd,mm,yy);
    get(dd,yy,mm);
    sort(s,s+cnt,cmp);
    for (int i = 0; i < cnt; i ++ )
    {
        // 出现相等的情况只输出一次
        if(i&&(s[i].yy==s[i-1].yy&&s[i].mm==s[i-1].mm&&s[i].dd==s[i-1].dd))continue;
        printf("%d-%02d-%02d\n",s[i].yy,s[i].mm,s[i].dd);
    }
    return 0;
}

1231. 航班时间

小 h 前往美国参加了蓝桥杯国际赛。

小 h 的女朋友发现小 h 上午十点出发,上午十二点到达美国,于是感叹到“现在飞机飞得真快,两小时就能到美国了”。

小 h 对超音速飞行感到十分恐惧。

仔细观察后发现飞机的起降时间都是当地时间。

由于北京和美国东部有 12 小时时差,故飞机总共需要 14 小时的飞行时间。

不久后小 h 的女朋友去中东交换。

小 h 并不知道中东与北京的时差。

但是小 h 得到了女朋友来回航班的起降时间。

小 h 想知道女朋友的航班飞行时间是多少。

对于一个可能跨时区的航班,给定来回程的起降时间。

假设飞机来回飞行时间相同,求飞机的飞行时间。

输入格式
一个输入包含多组数据。

输入第一行为一个正整数 T,表示输入数据组数。

每组数据包含两行,第一行为去程的起降时间,第二行为回程的起降时间。

起降时间的格式如下:

h1:m1:s1 h2:m2:s2
h1:m1:s1 h3:m3:s3 (+1)
h1:m1:s1 h4:m4:s4 (+2)
第一种格式表示该航班在当地时间h1时m1分s1秒起飞,在当地时间当日h2时m2分s2秒降落。

第二种格式表示该航班在当地时间h1时m1分s1秒起飞,在当地时间次日h2时m2分s2秒降落。

第三种格式表示该航班在当地时间h1时m1分s1秒起飞,在当地时间第三日h2时m2分s2秒降落。

输出格式
对于每一组数据输出一行一个时间hh:mm:ss,表示飞行时间为hh小时mm分ss秒。

注意,当时间为一位数时,要补齐前导零,如三小时四分五秒应写为03:04:05。

数据范围
保证输入时间合法(0≤h≤23,0≤m,s≤59),飞行时间不超过24小时。

输入样例:
3
17:48:19 21:57:24
11:05:18 15:14:23
17:21:07 00:31:46 (+1)
23:02:41 16:13:20 (+1)
10:19:19 20:41:24
22:19:04 16:41:09 (+1)
输出样例:
04:09:05
12:10:39
14:22:05

scanf格式识别读取的加深理解

小学奥数参考系:水速与船速
来回取木块[木块也会随着水流的速度移动] :
把参考系相对于水,则木块相对于水固定不变,时间仅看船速的来回



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

using namespace std;

int get_seconds(int h, int m, int s)//转换成描述
{
    return h * 3600 + m * 60 + s;
}

int get_time()
{
    string line;
    getline(cin, line);

    if (line.back() != ')') line += " (+0)";//统一(+天数)

    int h1, m1, s1, h2, m2, s2, d;
    sscanf(line.c_str(), "%d:%d:%d %d:%d:%d (+%d)", &h1, &m1, &s1, &h2, &m2, &s2, &d);//读入来回时间 和 天数d

    return get_seconds(h2, m2, s2) - get_seconds(h1, m1, s1) + d * 24 * 3600;//转换成秒数好计算 [一天24*3600 = 86400秒]  
}

int main()
{
    int n;
    scanf("%d", &n);
    string line;
    getline(cin, line);     // 忽略掉第一行的回车 !!!
    while (n -- )
    {
        int time = (get_time() + get_time()) / 2;//答案 == ( (end1-start1) + (end2 = start2) ) / 2
        int hour = time / 3600, minute = time % 3600 / 60, second = time % 60;
        printf("%02d:%02d:%02d\n", hour, minute, second);
    }

    return 0;
}


1241. 外卖店优先级

“饱了么”外卖系统中维护着 N 家外卖店,编号 1∼N。

每家外卖店都有一个优先级,初始时 (0 时刻) 优先级都为 0。

每经过 1 个时间单位,如果外卖店没有订单,则优先级会减少 1,最低减到 0;而如果外卖店有订单,则优先级不减反加,每有一单优先级加 2。

如果某家外卖店某时刻优先级大于 5,则会被系统加入优先缓存中;如果优先级小于等于 3,则会被清除出优先缓存。

给定 T 时刻以内的 M 条订单信息,请你计算 T 时刻时有多少外卖店在优先缓存中。

输入格式
第一行包含 3 个整数 N,M,T。

以下 M 行每行包含两个整数 ts 和 id,表示 ts 时刻编号 id 的外卖店收到一个订单。

输出格式
输出一个整数代表答案。

数据范围
1≤N,M,T≤105,
1≤ts≤T,
1≤id≤N
输入样例:
2 6 6
1 1
5 2
3 1
6 2
2 1
6 2
输出样例:
1
样例解释
6 时刻时,1 号店优先级降到 3,被移除出优先缓存;2 号店优先级升到 6,加入优先缓存。

所以是有 1 家店 (2 号) 在优先缓存中。

模拟题:暴力和优化[建议先会暴力(即没有优化)]
快递店暴力做法:st标记判断是否在队列,过程按题目操作模拟代码
在这里插入图片描述

优化:有一段时间没有订单:统一放到下一次有订单的时刻处理 : 则 1 0 5 ∗ 1 0 5 10^5*10^5 105105 --> 1 0 5 10^5 105:
在这里插入图片描述


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

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 100010;

int n, m, T;
int score[N], last[N];//score:当前优先级   最近一次订单时刻last[id]
bool st[N];//是否在缓存队列中

PII order[N];//双关键字排序: (时间ts, 订单id)    时间相同比id

int main()
{
    scanf("%d%d%d", &n, &m, &T);
    for (int i = 0; i < m; i ++ ) scanf("%d%d", &order[i].x, &order[i].y);
    sort(order, order + m);

    for (int i = 0; i < m;)
    {
        int j = i;//双指针
        while (j < m && order[j] == order[i]) j ++ ;//略过此id没有订单的时间段[相同id再次相遇时停止]
        int t = order[i].x, id = order[i].y, cnt = j - i;//没有订单的个数为cnt
        i = j;//i移动到j位置继续判断

        score[id] -= t - last[id] - 1;//减去没有订单数的时间段 ==  (此次订单时刻t - 上一次订单时刻last[id] - 1)
        if (score[id] < 0) score[id] = 0;//最小为0,不再减小
        if (score[id] <= 3) st[id] = false; // 【以上处理的是t时刻之前的信息】
        //t时刻后
        score[id] += cnt * 2;//此时刻有订单按规则乘二
        if (score[id] > 5) st[id] = true;//优先级 > 5 : 放入缓存队列          

        last[id] = t;//更新最近一次订单时刻为t
    }

    for (int i = 1; i <= n; i ++ )
        if (last[i] < T)//最后一段时间没有订单
        {
            score[i] -= T - last[i];//减去最后一段没有订单, 优先级--
            if (score[i] <= 3) st[i] = false;//判断优先级 <= 3 移除缓存队列
        }

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res += st[i];//计算最终在缓存队列中的个数

    printf("%d\n", res);

    return 0;
}

788. 逆序对的数量

给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。

逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。

输入格式
第一行包含整数 n,表示数列的长度。

第二行包含 n 个整数,表示整个数列。

输出格式
输出一个整数,表示逆序对的个数。

数据范围
1≤n≤100000,
数列中的元素的取值范围 [1,109]。

输入样例:
6
2 3 4 5 6 1
输出样例:
5
在这里插入图片描述

merge_sort 求逆序对
思路分两段L,R:i,j指针 归并过程判断R段中第j位元素小于L段中元素的数量(逆序)
则i后面均大于j i所在的段[l,mid] , 逆序对数量[i,mid] :res = mid - i + 1


#include <iostream>

using namespace std;

const int N = 1e6 + 10;
int tmp[N];
long long int res = 0;//注意逆序对数量大!!!

void merge_sort(int q[], int l, int r)  // 归并排序
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else
        {
            res += mid - i + 1;//计算逆序对数量long long
            tmp[k ++ ] = q[j ++ ];
        }
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}


int main()
{
    int n;
    cin >> n;
    int q[n];
    for (int i = 0; i < n; i ++) scanf("%d", &q[i]);
    merge_sort(q, 0, n - 1);
    cout << res << endl;
    return 0;

}

5.1 树状数组和线段树

1264. 动态求连续区间和

给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b] 的连续和。

输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。

第二行包含 n 个整数,表示完整数列。

接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。

数列从 1 开始计数。

输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。

输入样例:
10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8
输出样例:
11
30
35
树状数组是线段树(代码长)的代码简化版【竞赛使用版】

链接大佬滴图
在这里插入图片描述

树状树基本运用:
①在某个位置加上一个数[单点修改]
②快速求前缀和[区间查询]
[一个算法掌握其基本原理扩展应用场景【理解熟记】]

用图理解树状数组存值关系 : 图来源

二叉树:
在这里插入图片描述

树状数组本质二叉树,且特性每层父节点值等于对应所有叶子节点值的和:二进制末尾0个数等于层数
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

树状数组基本运用:
①在某个位置加上一个数[单点修改] :O(logn)
②快速求前缀和[区间查询] : O(logn)
扩:配合差分 : [区间修改]、[单点查询] (代码较复杂)

树状数组代码实现下标从1开始: 【末尾有几个0就在第几层
c[x] = (x - lowbit(x) , x] = [x - lowbit(x) + 1, x]
每一个区间长度 == R进制每轮的最后一位1所对应的次幂 == 第k轮:(x - 2^k, x] 【注意开闭区间】

图的理解:树状数组本质二叉树但是每层父节点存的值等于归属于父节点的子层的所有叶子节点的和 【归属的叶子节点下标范围 (x-lowbit(x),x-1]】


#include <cstdio>//scanf 不加也行。。
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int a[N], tr[N];//原数组, 树状数组tr:按定义赋初始值

int lowbit(int x)//lowbit(R) 返回R二进制最右边的1及其右边所有的0   
{
    return x & -x;
}
//单点修改:给a[x]加上v : 递归加上所有包含a[x]的tr项 【修改某个叶子节点:影响所有包含它的父节点:子节点二进制高位不断加lowbit[i]的父节点下标】
void add(int x, int v)//a += b -->  等效:加上结果c-原数a的差值 c = a + b , a += c - a
{
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;     //公式初始tr       
}
//查询区间和 (名称也可用sum) : 
int query(int x)//计算区间和 [1,x] 【每次加上(x - lowbit(x),x]的区间, 然后 x -= lowbit(i)减去最后的1 累加tr[i]则为区间[1,x]的和, 再用前缀和思想】              
{
    int res = 0;             
    for (int i = x; i; i -= lowbit(i)) res += tr[i];                         
    return res;                      
}       

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);               
    for (int i = 1; i <= n; i ++ ) add(i, a[i]);//读取a[i]的值初始化树状数组tr        

    while (m -- )
    {
        int k, x, y;
        scanf("%d%d%d", &k, &x, &y);
        if (k == 0) printf("%d\n", query(y) - query(x - 1));//前缀和思想 [x,y]区间和 = s[y] - s[x - 1]
        else add(x, y);//在x位加上y
    }

    return 0; 
}

线段树的写法
x的父节点 ⌊ x 2 ⌋ \lfloor \frac{x}{2} \rfloor 2x对应位运算 x > > 1 x>>1 x>>1
x的左儿子:   2 ∗ x ~2 * x  2x对应位运算 x < < 1 x<<1 x<<1
x的右儿子:   2 ∗ x + 1 ~2 * x + 1  2x+1对应位运算 x < < 1   ∣   1 x<<1~|~1 x<<1  1

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

using namespace std;

const int N = 100010;

int n, m;
int w[N];
struct Node
{
    int l, r;
    int sum;
}tr[N * 4]; //【节点个数4N】

void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;  //父区间和 = 左右子区间和
}

void build(int u, int l, int r) //根节点编号, 初始化区间
{
    if (l == r) tr[u] = {l, r, w[r]}; //叶子节点 - 初始赋值结构体 - 最底层单点区间 - 左右端点为自身编号
    else
    {
        tr[u] = {l, r}; //二路递归划分区间 初始赋值父节点u的左右孩子编号为l, r
        int mid = l + r >> 1; //中间划分[此处递归数组区间左右端点] 
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);  //左右孩子作为根 - 递归划分                   
        pushup(u); //子节点 更新 父节点 (用叶子节点赋值给父节点)
    }
}

int query(int u, int l, int r) //根节点编号, 查询区间
{   //tr[u].l >= l && tr[u].r <= r
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum; //当前区间被完全包含(属于查询区间的子集) - 递归累加
    int mid = tr[u].l + tr[u].r >> 1; //【mid :线段树中u的子树中的中点位置】
    int sum = 0;  //
    if (l <= mid) sum = query(u << 1, l, r); //若左边有交集,加上子区间       (= 或 += 都行)
    if (r > mid) sum += query(u << 1 | 1, l, r); //与子区间是否有交集-(区间右端点(mid + 1) <= r则有交集,加上)
    return sum; 
}

void modify(int u, int x, int v) //从根u开始 修改编号为x的节点.sum += v, 修改所有包含x节点的区间
{   //找到叶子节点x : tr[u].l == x && tr[u].r == x(必能找到-> 到叶子节点l==r就是节点x) 
    if (tr[u].l == tr[u].r) tr[u].sum += v;  //包含叶子节点编号x的区间.sum += v
    else
    {   //二分查询 - 包含节点编号x的位置
        int mid = tr[u].l + tr[u].r >> 1;  //【mid :线段树中u的子树中的中点位置】
        if (x <= mid) modify(u << 1, x, v);    
        else modify(u << 1 | 1, x, v);
        pushup(u); //修改完单个节点 - 向上更新所有包含x的区间
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
    build(1, 1, n); //建立线段树

    int k, a, b;
    while (m -- )
    {
        scanf("%d%d%d", &k, &a, &b);
        if (k == 0) printf("%d\n", query(1, a, b)); //[区间查询]从根节点1开始查询sum[a, b]
        else modify(1, a, b); //[单点修改] - 对应线段树中包含此叶子节点的父节点均需修改
    }

    return 0;
}

1265. 数星星

天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。

如果一个星星的左下方(包含正左和正下)有 k 颗星星,就说这颗星星是 k 级的。

在这里插入图片描述

例如,上图中星星 5 是 3 级的(1,2,4 在它左下),星星 2,4 是 1 级的。

例图中有 1 个 0 级,2 个 1 级,1 个 2 级,1 个 3 级的星星。

给定星星的位置,输出各级星星的数目。

换句话说,给定 N 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。

输入格式
第一行一个整数 N,表示星星的数目;

接下来 N 行给出每颗星星的坐标,坐标用两个整数 x,y 表示;

不会有星星重叠。星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出。

输出格式
N 行,每行一个整数,分别是 0 级,1 级,2 级,……,N−1 级的星星的数目。

数据范围
1≤N≤15000,
0≤x,y≤32000
输入样例:
5
1 1
5 1
7 1
3 3
5 5
输出样例:
1
2
1
1
0


运用数据结构难点:怎么提取模型 ==> 看操作 : 边插入边求前缀和 ==> 树状数组
在这里插入图片描述

题目:y 坐标增序给出,y 坐标相同的按 x 坐标增序给出–> 推出只要前面的x小于当前x就算数量+1

求对应等级转化操作:
①初始化输入星星对应个数 :单点修改: a[i] ++
②统计星星按层分区间的每层个数和 :计算前缀和 :a[1] ~ a[x] (在左下方所有星星-对应为所有子节点-前缀和)

做题细节:树状数组从1开始而题目下标从0开始==>采用统一偏移 :把所有下标向右平移1个单位: x++

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

using namespace std;

const int N = 32010;

int n;
int tr[N], level[N];

int lowbit(int x)
{
    return x & -x;
}

void add(int x)
{   //佬:要把整个树状数组进行更新,n只是星星的数量,星星的数量和x的范围是没有关系的。
    for (int i = x; i < N; i += lowbit(i)) tr[i] ++ ;//x的整个范围是N,所以要把整个范围内的树状数组更新一下。
}

int sum(int x)//返回前缀和[1,x]
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int x, y;
        scanf("%d%d", &x, &y);
        x ++ ;//下标0开始偏移到下标1开始!!!
        level[sum(x)] ++ ;//对应等数++【星星x的左下方和正下方范围内星星数量== 区间和】
        add(x);//添加星星,更新树状数组
    }

    for (int i = 0; i < n; i ++ ) printf("%d\n", level[i]);//输出的时候不能减一,而应该输出level[1 ~ n],因为每个星星的等级都提高了一级。

    return 0;
}

1270. 数列区间最大值

时/空限制:2s / 64MB
输入一串数字,给你 M 个询问,每次询问就给你两个数字 X,Y,要求你说出 X 到 Y 这段区间内的最大数。

输入格式
第一行两个整数 N,M 表示数字的个数和要询问的次数;

接下来一行为 N 个数;

接下来 M 行,每行都有两个整数 X,Y。

输出格式
输出共 M 行,每行输出一个数。

数据范围
1≤N≤ 1 0 5 10^5 105,
1≤M≤ 1 0 6 10^6 106,
1≤X≤Y≤N,
数列中的数字均不超过 2 31 − 1 2^{31}−1 2311
输入样例:
10 2
3 2 4 5 6 8 1 2 9 7
1 4
3 8
输出样例:
5
8

线段树求区间最大值:属性sum改为max_value, 没有修改元素,也没有计算区间和,不用pushup

#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int n, m;
int w[N];
struct Node
{
    int l, r;
    int maxv; //区间max_value
}tr[N * 4];

void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l, r, w[r]};
    else
    {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        tr[u].maxv = max(tr[u << 1].maxv, tr[u << 1 | 1].maxv);
    }
}

int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].maxv;
    int mid = tr[u].l + tr[u].r >> 1;
    int maxv = INT_MIN; //include <climits>
    if (l <= mid) maxv = query(u << 1, l, r);
    if (r > mid) maxv = max(maxv, query(u << 1 | 1, l, r));
    return maxv;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]); //【从1开始】
    build(1, 1, n);

    int l, r;
    while (m -- )
    {
        scanf("%d%d", &l, &r);
        printf("%d\n", query(1, l, r));
    }

    return 0;
}

5.2 树状数组和线段树——习题课

1215. 小朋友排队

n 个小朋友站成一排。

现在要把他们按身高从低到高的顺序排列,但是每次只能交换位置相邻的两个小朋友。

每个小朋友都有一个不高兴的程度。

开始的时候,所有小朋友的不高兴程度都是 0。

如果某个小朋友第一次被要求交换,则他的不高兴程度增加 1,如果第二次要求他交换,则他的不高兴程度增加 2(即不高兴程度为 3),依次类推。当要求某个小朋友第 k 次交换时,他的不高兴程度增加 k。

请问,要让所有小朋友按从低到高排队,他们的不高兴程度之和最小是多少。

如果有两个小朋友身高一样,则他们谁站在谁前面是没有关系的。

输入格式
输入的第一行包含一个整数 n,表示小朋友的个数。

第二行包含 n 个整数 H1,H2,…,Hn,分别表示每个小朋友的身高。

输出格式
输出一行,包含一个整数,表示小朋友的不高兴程度和的最小值。

数据范围
1≤n≤100000,
0≤Hi≤1000000
输入样例:
3
3 2 1
输出样例:
9
样例解释
首先交换身高为3和2的小朋友,再交换身高为3和1的小朋友,再交换身高为2和1的小朋友,每个小朋友的不高兴程度都是3,总和为9。

贪心 + 树状数组统计区间元素个数
在h[i]下标后面比这个数小的 + 在h[i]下标前面且比这个数小的 == h[i]被交换的次数
三种解法

//逆序对数量k == 冒泡排序交换次数k
//此题树状数组sum函数存个数,10万级别不会爆int

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

using namespace std;

typedef long long LL;

const int N = 1000010;

int n;
int h[N], tr[N];//存高度h
int sum[N];//sum = 在此数下标后面比这个数小的 + 在此数下标前面且比这个数小的 == 此数被交换的次数

int lowbit(int x)
{
    return x & -x;
}

void add(int x, int v)
{
    for (int i = x; i < N; i += lowbit(i)) tr[i] += v;
}

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

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &h[i]), h[i] ++ ;//先把所有h[i]++ 则值域落在[1,h[]max]

    // 求每个数前面有多少个数比它大
    for (int i = 0; i < n; i ++ )//从前往后边读入边统计 :过程中把每个身高的人数记下来了,所以可以查询小于当前身高的人数
    {
        sum[i] = query(N - 1) - query(h[i]);//在后面即值更大的区间的数的个数 [左开右闭区间]
        add(h[i], 1);//包含h[i]的区间 += 1
    }

    // 每个数后面有多少个数比它小
    memset(tr, 0, sizeof tr);
    for (int i = n - 1; i >= 0; i -- )//从后往前边读入边统计,过程中把每个身高的人数记下来了,所以可以查询大于当前身高的人数
    {
        sum[i] += query(h[i] - 1);//在前面的小于h[i] 即 [1, h[i] - 1]
        add(h[i], 1);
    }

    LL res = 0; //第i个小朋友怒气值 = 1 + 2 + ... sum[i] = (LL)sum[i] * (sum[i] + 1) / 2
    for (int i = 0; i < n; i ++ ) res += (LL)sum[i] * (sum[i] + 1) / 2; //注意计算交换次数可能爆int  : 加(LL)

    cout << res << endl;

    return 0;
}

思路[贪心]:若某个小朋友构成k对逆序对,则这个小朋友至少交换k次。
暴力

#include <cstdio>
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int h[N];

int main()
{
    scanf("%d", &n);
    for(int i = 0; i < n; i++) scanf("%d", &h[i]);

    LL ans = 0;

    // 遍历n个小朋友
    for(int i = 0; i < n; i++)
    {
        int res = 0;    // i构成多少个逆序对
        // 找i前面大于它的人数
        for(int j = 0; j < i; j++)
            if(h[j] > h[i])
                res++;

        // 找i后面小于它的个数
        for(int j = i + 1; j < n; j++)
            if(h[j] < h[i])
                res++;


        ans += res * (res + 1) / 2;        
    }

    cout << ans << endl;

    return 0;
}

1228. 油漆面积

X星球的一批考古机器人正在一片废墟上考古。

该区域的地面坚硬如石、平整如镜。

管理人员为方便,建立了标准的直角坐标系。

每个机器人都各有特长、身怀绝技。

它们感兴趣的内容也不相同。

经过各种测量,每个机器人都会报告一个或多个矩形区域,作为优先考古的区域。

矩形的表示格式为 (x1,y1,x2,y2),代表矩形的两个对角点坐标。

为了醒目,总部要求对所有机器人选中的矩形区域涂黄色油漆。

小明并不需要当油漆工,只是他需要计算一下,一共要耗费多少油漆。

其实这也不难,只要算出所有矩形覆盖的区域一共有多大面积就可以了。

注意,各个矩形间可能重叠。

输入格式
第一行,一个整数 n,表示有多少个矩形。

接下来的 n 行,每行有 4 个整数 x1,y1,x2,y2,空格分开,表示矩形的两个对角顶点坐标。

输出格式
一行一个整数,表示矩形覆盖的总面积。

数据范围
1≤n≤10000,
0≤x1,x2,y2,y2≤10000
数据保证 x1<x2 且 y1<y2。

输入样例1:
3
1 5 10 10
3 1 20 20
2 7 15 17
输出样例1:
340
输入样例2:
3
5 2 10 6
2 7 12 10
8 1 15 15
输出样例2:
128

1232. 三体攻击

三体人将对地球发起攻击。

为了抵御攻击,地球人派出了 A × B × C A×B×C A×B×C 艘战舰,在太空中排成一个 A 层 B 行 C 列的立方体。

其中,第 i 层第 j 行第 k 列的战舰(记为战舰 (i,j,k))的生命值为 d(i,j,k)。

三体人将会对地球发起 m 轮“立方体攻击”,每次攻击会对一个小立方体中的所有战舰都造成相同的伤害。

具体地,第 t 轮攻击用 7 个参数 l a t , r a t , l b t , r b t , l c t , r c t , h t la_t,ra_t,lb_t,rb_t,lc_t,rc_t,h_t lat,rat,lbt,rbt,lct,rct,ht 描述;

所有满足 i∈[ l a t , r a t la_t,ra_t lat,rat],j∈[ l b t , r b t lb_t,rb_t lbt,rbt],k∈[ l c t , r c t lc_t,rc_t lct,rct] 的战舰 (i,j,k) 会受到 h t h_t ht的伤害。

如果一个战舰累计受到的总伤害超过其防御力,那么这个战舰会爆炸。

地球指挥官希望你能告诉他,第一艘爆炸的战舰是在哪一轮攻击后爆炸的。

输入格式
第一行包括 4 个正整数 A,B,C,m;

第二行包含 A × B × C A×B×C A×B×C 个整数,其中第 ( ( i − 1 ) × B + ( j − 1 ) ) × C + ( k − 1 ) + 1 ((i−1)×B+(j−1))×C+(k−1)+1 ((i1)×B+(j1))×C+(k1)+1 个数为 d ( i ,   j ,   k ) d(i, j, k) d(i,j,k)

第 3 到第 m+2 行中,第 (t − 2) 行包含 7 个正整数 l a t ,   r a t ,   l b t ,   r b t ,   l c t ,   r c t ,   h t la_t, ra_t, lb_t, rb_t, lc_t, rc_t, h_t lat,rat,lbt,rbt,lct,rct,ht

输出格式
输出第一个爆炸的战舰是在哪一轮攻击后爆炸的。

保证一定存在这样的战舰。

数据范围
1 ≤ A × B × C ≤ 1 0 6 , 1≤A×B×C≤10^6, 1A×B×C106,
1 ≤ m ≤ 1 0 6 , 1≤m≤10^6, 1m106,
0 ≤ d ( i ,   j ,   k ) ,   h t ≤ 1 0 9 , 0≤d(i, j, k), h_t≤10^9, 0d(i,j,k),ht109,
1 ≤ l a t ≤ r a t ≤ A , 1≤la_t≤ra_t≤A, 1latratA,
1 ≤ l b t ≤ r b t ≤ B , 1≤lb_t≤rb_t≤B, 1lbtrbtB,
1 ≤ l c t ≤ r c t ≤ C 1≤lc_t≤rc_t≤C 1lctrctC
层、行、列的编号都从 1 开始。

输入样例:
2 2 2 3
1 1 1 1 1 1 1 1
1 2 1 2 1 1 1
1 1 1 2 1 2 1
1 1 1 1 1 1 2
输出样例:
2
样例解释
在第 2 轮攻击后,战舰 (1,1,1) 总共受到了 2 点伤害,超出其防御力导致爆炸。

1237. 螺旋折线

如下图所示的螺旋折线经过平面上所有整点恰好一次。

在这里插入图片描述

对于整点 (X,Y),我们定义它到原点的距离 dis(X,Y) 是从原点到 (X,Y) 的螺旋折线段的长度。

例如 dis(0,1)=3,dis(−2,−1)=9

给出整点坐标 (X,Y),你能计算出 dis(X,Y) 吗?

输入格式
包含两个整数 X,Y。

输出格式
输出一个整数,表示 dis(X,Y)。

数据范围
− 1 0 9 ≤ X , Y ≤ 1 0 9 −10^9≤X,Y≤10^9 109X,Y109
输入样例:
0 1
输出样例:
3

797. 差分

输入一个长度为 n 的整数序列。

接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。

请你输出进行完所有操作后的序列。

输入格式
第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。

输出格式
共一行,包含 n 个整数,表示最终序列。

数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2

差分:前缀和逆运算
原数组a,差分数组b
使得 a[i] = b[1] + b[2 ] + … + b[i] ==> b[i] = a[i] - a[i-1]
不直接对原数组a运算,而是用差分数组b运算 ,最后再赋值给a ==> a[i] = a[i - 1] + b[i]


#include<iostream>

using namespace std;

const int N= 100010;

int n, m;
int a[N], b[N];

int main()
{
    cin >> n >> m; 
    for(int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]); 
        b[i] = a[i] - a[i - 1];//用到前缀和构造差分b[i],有i-1下标,下标从1开始
    }
    int l, r, c;
    while (m--)
    {
        scanf("%d%d%d", &l, &r, &c);
        b[l] += c, b[r + 1] -= c;     //将序列中[l, r]之间的每个数都加上c
    }
     for (int i = 1; i <= n; i++)
    {
        a[i] = b[i] + a[i - 1];    //前缀和运算
        printf("%d ", a[i]);
    }
    return 0;
}

//作者:林小鹿 链接:https://www.acwing.com/solution/content/26588/


//y总 节省原数组a
#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int s[N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> s[i];
    for (int i = n; i; i -- ) s[i] -= s[i - 1];//逆序赋值求差分数组b[i] = a[i] - a[i - 1]

    while (m -- )
    {
        int l, r, c;
        cin >> l >> r >> c;
        s[l] += c, s[r + 1] -= c;
    }

    for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];//前缀和数组a[i] = b[i] + a[i - 1]

    for (int i = 1; i <= n; i ++ ) cout << s[i] << ' ';
    cout << endl;

    return 0;
}


//作者:yxc 链接:https://www.acwing.com/activity/content/code/content/174383/

/*
void insert(int l,int r, int c)
{
    b[l] += c, b[r + 1] -= c;
}
*/

798. 差分矩阵

输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 c。

请你将进行完所有操作后的矩阵输出。

输入格式
第一行包含整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含 5 个整数 x1,y1,x2,y2,c,表示一个操作。

输出格式
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。

数据范围
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2

二维差分
佬の题解

佬の图片:
在这里插入图片描述


#include <iostream>

using namespace std;

typedef long long ll;

const int N = 1010;
const int inf = 0x3f3f3f;

int n, m, q;
int a[N][N];

int main()
{
    scanf("%d%d%d", &n, &m, &q);

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )//初始化二维差分数组
        {
            int x;
            scanf("%d", &x);
            a[i][j] += x;
            a[i + 1][j] -= x;
            a[i][j + 1] -= x;
            a[i + 1][j + 1] += x;
        }

    while (q -- )
    {
        int x1, y1, x2, y2, c;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        a[x1][y1] += c;
        a[x2 + 1][y1] -= c;
        a[x1][y2 + 1] -= c;
        a[x2 + 1][y2 + 1] += c;
    }

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];//把差分数组转换成前缀和数组

    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ ) printf("%d ", a[i][j]);
        puts("");
    }

    return 0;
}

6.1 双指针、BFS和图论

1238. 日志统计

小明维护着一个程序员论坛。现在他收集了一份”点赞”日志,日志共有 N 行。

其中每一行的格式是:

ts id
表示在 ts 时刻编号 id 的帖子收到一个”赞”。

现在小明想统计有哪些帖子曾经是”热帖”。

如果一个帖子曾在任意一个长度为 D 的时间段内收到不少于 K 个赞,小明就认为这个帖子曾是”热帖”。

具体来说,如果存在某个时刻 T 满足该帖在 [T,T+D) 这段时间内(注意是左闭右开区间)收到不少于 K 个赞,该帖就曾是”热帖”。

给定日志,请你帮助小明统计出所有曾是”热帖”的帖子编号。

输入格式
第一行包含三个整数 N,D,K。

以下 N 行每行一条日志,包含两个整数 ts 和 id。

输出格式
按从小到大的顺序输出热帖 id。

每个 id 占一行。

数据范围
1≤K≤N≤105,
0≤ts,id≤105,
1≤D≤10000
输入样例:
7 10 2
0 1
0 10
10 10
10 1
9 1
100 3
100 3
输出样例:
1
3

暴力法:在这里插入图片描述

优化
双指针[j,i]理解为滑动窗口
维护大小为d的窗口

//有重复统计,就有优化【类似滑动窗口,进去一个+出来一个】
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

#define x first 
#define y second

using namespace std;

typedef pair<int, int> PII; 

const int N = 100010;

int n, d, k;
PII logs[N]; // (ts,id)
int cnt[N];
bool st[N];  // 记录每个帖子是否是热帖

int main()
{
    scanf("%d%d%d", &n, &d, &k);
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &logs[i].x, &logs[i].y);

    sort(logs, logs + n);//【按时间ts排序】

    for (int i = 0, j = 0; i < n; i ++ )//[j, i]区间长度 <= d
    {
        int id = logs[i].y; 
        cnt[id] ++ ; //统计区间内对应id收到的赞

        while (logs[i].x - logs[j].x >= d) //if(i位置当前赞 - j位置前一次赞 >= d时间跨度[区间长度限制]) j向前移动 【看做滑动窗口理解,窗口大小 = d】
        {
            cnt[logs[j].y] -- ;//j向前移动位置 ,原本的j位置出窗口, i在循环中i++  [类比最长连续不重复子序列]
            j ++ ;
        }

        if (cnt[id] >= k) st[id] = true;  //id在满足小于时间跨度[区间长度限制]d获得>=k个赞
    }

    for (int i = 0; i <= 100000; i ++ )//输出满足热度的帖子的id
        if (st[i])
            printf("%d\n", i);

    return 0;
}

1101. 献给阿尔吉侬的花束

阿尔吉侬是一只聪明又慵懒的小白鼠,它最擅长的就是走各种各样的迷宫。

今天它要挑战一个非常大的迷宫,研究员们为了鼓励阿尔吉侬尽快到达终点,就在终点放了一块阿尔吉侬最喜欢的奶酪。

现在研究员们想知道,如果阿尔吉侬足够聪明,它最少需要多少时间就能吃到奶酪。

迷宫用一个 R×C 的字符矩阵来表示。

字符 S 表示阿尔吉侬所在的位置,字符 E 表示奶酪所在的位置,字符 # 表示墙壁,字符 . 表示可以通行。

阿尔吉侬在 1 个单位时间内可以从当前的位置走到它上下左右四个方向上的任意一个位置,但不能走出地图边界。

输入格式
第一行是一个正整数 T,表示一共有 T 组数据。

每一组数据的第一行包含了两个用空格分开的正整数 R 和 C,表示地图是一个 R×C 的矩阵。

接下来的 R 行描述了地图的具体内容,每一行包含了 C 个字符。字符含义如题目描述中所述。保证有且仅有一个 S 和 E。

输出格式
对于每一组数据,输出阿尔吉侬吃到奶酪的最少单位时间。

若阿尔吉侬无法吃到奶酪,则输出“oop!”(只输出引号里面的内容,不输出引号)。

每组数据的输出结果占一行。

数据范围
1<T≤10,
2≤R,C≤200
输入样例:

3
3 4
.S..
###.
..E.
3 4
.S..
.E..
....
3 4
.S..
####
..E.

输出样例:
5
1
oop!

经典迷宫BFS

记bfs步骤:
	①初始化队列q和dist取-1 ,dist[start.x][start.y] = 0(x,y为define全局定义)
	 起点start放入队列,方向向量dx[4](从-1开始)  ,dy[4]
	②遍历所有元素依次入队,while(队不为空)
	③取队头,出队 , 遍历所有方向 ,依据题目判断边界,条件
	④放入对列 , 中间加个if(end== make_pair(x, y) ) return dist[x][y];判断直接结束
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

#define x first//pair的代码简化
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 210;

int n, m;
char g[N][N];
int dist[N][N];


int bfs(PII start, PII end)//bfs模板【STL队列版】
{
    queue<PII> q;
    memset(dist, -1, sizeof dist);//不可达则未更新,标记为-1
                
    dist[start.x][start.y] = 0;//0标记走过 
    q.push(start);//起点入队                                    
	
	
	
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//dx[4](从-1开始)模式化

    while (q.size())                                       
    {
        auto t = q.front();//取队头                                                       
        q.pop();//出队                                                                                       

        for (int i = 0; i < 4; i ++ )
        {
            int x = t.x + dx[i], y = t.y + dy[i];
            if (x < 0 || x >= n || y < 0 || y >= m || g[x][y] == '#' || dist[x][y] != -1) continue; // 出界 || 障碍物 || 已经走过 --> 判断下一个 

            dist[x][y] = dist[t.x][t.y] + 1;

            if (end == make_pair(x, y)) return dist[x][y];   //make_pair返回pair    ,也可以放在最后返回dist

            q.push({x, y});
        }
    }
   	
    return -1;
}


int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d%d", &n, &m);
        for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);//按字符串读入每行

        PII start, end;//设置终点、起点, 寻找终点、起点
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < m; j ++ )
                if (g[i][j] == 'S') start = {i, j};
                else if (g[i][j] == 'E') end = {i, j};

        int distance = bfs(start, end);//起点--->终点     
        if (distance == -1) puts("oop!");//返回-1未更新,不可达
        else printf("%d\n", distance);
    }

    return 0;
}

不错的思路

1113. 红与黑

有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。

你站在其中一块黑色的瓷砖上,只能向相邻(上下左右四个方向)的黑色瓷砖移动。

请写一个程序,计算你总共能够到达多少块黑色的瓷砖。

输入格式
输入包括多个数据集合。

每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y 方向瓷砖的数量。

在接下来的 H 行中,每行包括 W 个字符。每个字符表示一块瓷砖的颜色,规则如下

1)‘.’:黑色的瓷砖;
2)‘#’:红色的瓷砖;
3)‘@’:黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一次。

当在一行中读入的是两个零时,表示输入结束。

输出格式
对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。

数据范围
1≤W,H≤20
输入样例:

6 9 
....#. 
.....# 
...... 
...... 
...... 
...... 
...... 
#@...# 
.#..#. 
0 0

输出样例:
45

初学搜索算法:
整个棋盘看做一个整体能否从一种棋盘状态转移到另一种棋盘状态 【需要回溯】
棋盘内每个元素为整体, 改变棋盘内元素转移到一种棋盘状态【不用回溯】

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 25;

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

//int ne[4][2] = {{-1,0}, {0,1}, {1,0}, {0,-1}};
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dfs(int x, int y)//起点开始
{
    int cnt = 1;

    st[x][y] = true;
    for (int i = 0; i < 4; i ++ )
    {
        int a = x + dx[i], b = y + dy[i];
        if (a < 0 || a >= n || b < 0 || b >= m) continue;//合并: if(越界 || 不是黑色 || 标记走过) continue; 判断下一个  
        if (g[a][b] != '.') continue;
        if (st[a][b]) continue;

        cnt += dfs(a, b);//能到的点的数量
    }

    return cnt;
}

int main()
{
    while (cin >> m >> n, n || m) //所有表达式都会执行,只不过返回值是最后一个表达式的值
    {
        for (int i = 0; i < n; i ++ ) cin >> g[i];

        int x, y;
        for (int i = 0; i < n; i ++ )
            for (int j = 0; j < m; j ++ )
                if (g[i][j] == '@')//起点
                {
                    x = i;
                    y = j;
                }

        memset(st, 0, sizeof st);//多组数据,每次要把标记数组清空一遍
        cout << dfs(x, y) << endl;
    }

    return 0;
}

1224. 交换瓶子

有 N 个瓶子,编号 1∼N,放在架子上。

比如有 5 个瓶子:

2 1 3 5 4
要求每次拿起 2 个瓶子,交换它们的位置。

经过若干次后,使得瓶子的序号为:

1 2 3 4 5
对于这么简单的情况,显然,至少需要交换 2 次就可以复位。

如果瓶子更多呢?你可以通过编程来解决。

输入格式
第一行包含一个整数 N,表示瓶子数量。

第二行包含 N 个整数,表示瓶子目前的排列状况。

输出格式
输出一个正整数,表示至少交换多少次,才能完成排序。

数据范围
1≤N≤10000,

输入样例1:
5
3 1 2 5 4
输出样例1:
3
输入样例2:
5
5 4 3 2 1
输出样例2:
2

另一题小朋友排身高[只交换相邻元素] ,此题任意位置交换
又如合并果子与石子合并的区别

看成图:下标(从1开始)(指向)–>值 [特点:入度、出度最多1,存在多个环]
最终状态:变成自环 ==> [每个位置:下标 == 值 ]
①交换环内的点 :裂成两个环 ②交换两个不同环的点:合并成一个环

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010;

int n;
int b[N];//绝妙思想:看成坐标值指向元素值
bool st[N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]);

    int cnt = 0;
    for (int i = 1; i <= n; i ++ )
        if (!st[i])
        {
            cnt ++ ;
            for (int j = i; !st[j]; j = b[j]) //遍历链表の感觉
                st[j] = true;//标记已在环中使用
        }

    printf("%d\n", n - cnt);//初始cnt个环,最终要变成n个环,每次操作最多增加一个环 : 至少操作 n - cnt 次

    return 0;
}

6.2 双指针、BFS和图论——习题课

1240. 完全二叉树的权值

给定一棵包含 N 个节点的完全二叉树,树上每个节点都有一个权值,按从上到下、从左到右的顺序依次是 A1,A2,⋅⋅⋅AN,如下图所示:

在这里插入图片描述

现在小明要把相同深度的节点的权值加在一起,他想知道哪个深度的节点权值之和最大?

如果有多个深度的权值和同为最大,请你输出其中最小的深度。

注:根的深度是 1。

输入格式
第一行包含一个整数 N。

第二行包含 N 个整数 A1,A2,⋅⋅⋅AN。

输出格式
输出一个整数代表答案。

数据范围
1≤N≤ 1 0 5 10^5 105,
1 0 5 10^5 105≤Ai≤ 1 0 5 10^5 105
输入样例:
7
1 6 5 4 3 2 1
输出样例:
2
在这里插入图片描述

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

using namespace std;

typedef long long LL;//数据 (10^5)^2  大于int范围 

const int N = 100010;

int n;
int a[N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    LL maxs = -1e18;//找最大值, 初始化小于数据范围最小值(按题目小于-1e-5即可)
    int depth = 0;

    for (int d = 1, i = 1; i <= n; i *= 2, d ++ ) //d深度(层数), 两层循环但是O(n):每个点只算一次 【双指针】  
    {
        LL s = 0;//LL
        for (int j = i; j < i + (1 << d - 1) && j <= n; j ++ ) //每层元素:[i, i + 2^(d-1)] 用位运算快!&&不能超过总数n
        s += a[j];

        if (s > maxs) 
        {
            maxs = s; //最大值
            depth = d; //对应深度(依题意:根从1开始)
        }
    }

    printf("%d\n", depth);

    return 0;
}

1096. 地牢大师

你现在被困在一个三维地牢中,需要找到最快脱离的出路!

地牢由若干个单位立方体组成,其中部分不含岩石障碍可以直接通过,部分包含岩石障碍无法通过。

向北,向南,向东,向西,向上或向下移动一个单元距离均需要一分钟。

你不能沿对角线移动,迷宫边界都是坚硬的岩石,你不能走出边界范围。

请问,你有可能逃脱吗?

如果可以,需要多长时间?

输入格式
输入包含多组测试数据。

每组数据第一行包含三个整数 L,R,C 分别表示地牢层数,以及每一层地牢的行数和列数。

接下来是 L 个 R 行 C 列的字符矩阵,用来表示每一层地牢的具体状况。

每个字符用来描述一个地牢单元的具体状况。

其中, 充满岩石障碍的单元格用”#”表示,不含障碍的空单元格用”.”表示,你的起始位置用”S”表示,终点用”E”表示。

每一个字符矩阵后面都会包含一个空行。

当输入一行为”0 0 0”时,表示输入终止。

输出格式
每组数据输出一个结果,每个结果占一行。

如果能够逃脱地牢,则输出”Escaped in x minute(s).”,其中X为逃脱所需最短时间。

如果不能逃脱地牢,则输出”Trapped!”。

数据范围
1≤L,R,C≤100
输入样例:

3 4 5
S....
.###.
.##..
###.#

#####
#####
##.##
##...

#####
#####
#.###
####E

1 3 3
S##
#E#
###

0 0 0

输出样例:
Escaped in 11 minute(s).
Trapped!

1233. 全球变暖

你有一张某海域 N×N 像素的照片,”.”表示海洋、”#”表示陆地,如下所示:

.......
.##....
.##....
....##.
..####.
...###.
.......

其中”上下左右”四个方向上连在一起的一片陆地组成一座岛屿,例如上图就有 2 座岛屿。

由于全球变暖导致了海面上升,科学家预测未来几十年,岛屿边缘一个像素的范围会被海水淹没。

具体来说如果一块陆地像素与海洋相邻(上下左右四个相邻像素中有海洋),它就会被淹没。

例如上图中的海域未来会变成如下样子:

.......
.......
.......
.......
....#..
.......
.......

请你计算:依照科学家的预测,照片中有多少岛屿会被完全淹没。

输入格式
第一行包含一个整数N。

以下 N 行 N 列,包含一个由字符”#”和”.”构成的 N×N 字符矩阵,代表一张海域照片,”#”表示陆地,”.”表示海洋。

照片保证第 1 行、第 1 列、第 N 行、第 N 列的像素都是海洋。

输出格式
一个整数表示答案。

数据范围
1≤N≤1000
输入样例1:

7
.......
.##....
.##....
....##.
..####.
...###.
.......

输出样例1:
1
输入样例2:

9
.........
.##.##...
.#####...
.##.##...
.........
.##.#....
.#.###...
.#..#....
.........

输出样例2:
1

BFS搜连通块

//①BFS可求连通块数量
//②bound:四周有海bound++, 连通块内格子总数total  : total == bound 全部被淹没 cnt++
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

#define x first//简化pair代码
#define y second

using namespace std;

typedef pair<int, int> PII;//存坐标(x, y)

const int N = 1010;

int n;//行列
char g[N][N];//地图
bool st[N][N];//标记搜过为true
PII q[N * N];//BFS需队列
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};

void bfs(int sx, int sy, int &total, int &bound)//起点(sx, sy)(从岛屿元素开始搜), 要修改值判断加&   
{
    int hh = 0, tt = 0;
    q[0] = {sx, sy};
    st[sx][sy] = true;//起点岛屿

    while (hh <= tt)
    {
        PII t = q[hh ++ ];//取队头

        total ++ ;//起点岛屿, 初始total = 1
        bool is_bound = false;
        for (int i = 0; i < 4; i ++ )//判断上下左右是否靠海
        {
            int x = t.x + dx[i], y = t.y + dy[i];
            if (x < 0 || x >= n || y < 0 || y >= n) continue;  // 出界
            if (st[x][y]) continue;//被搜过, 判下一个方向
            if (g[x][y] == '.')
            {
                is_bound = true;//周围有海
                continue;
            }

            q[ ++ tt] = {x, y};//入队, 同时存{x,y}岛屿元素坐标作为下一个队头判断
            st[x][y] = true;//标记搜过,不会重复计算岛屿元素
        }

        if (is_bound) bound ++ ;//每轮一个坐标, 若被淹没bound++
    }
}

int main()
{
    scanf("%d", &n);

    for (int i = 0; i < n; i ++ ) scanf("%s", g[i]);

    int cnt = 0;//淹没的岛屿数量
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            if (!st[i][j] && g[i][j] == '#')//没被搜过 && 是陆地
            {
                int total = 0, bound = 0;
                bfs(i, j, total, bound);
                if (total == bound) cnt ++ ;//岛屿中全部的total都是is_bound(被淹没) , 被淹没的岛屿数量++   
            }

    printf("%d\n", cnt);

    return 0;
}

1207. 大臣的旅费

很久以前,T王国空前繁荣。

为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市。

为节省经费,T国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达。

同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的。

J是T国重要大臣,他巡查于各大城市之间,体察民情。

所以,从一个城市马不停蹄地到另一个城市成了J最常做的事情。

他有一个钱袋,用于存放往来城市间的路费。

聪明的J发现,如果不在某个城市停下来修整,在连续行进过程中,他所花的路费与他已走过的距离有关,在走第x千米到第x+1千米这一千米中(x是整数),他花费的路费是x+10这么多。也就是说走1千米花费11,走2千米要花费23。

J大臣想知道:他从某一个城市出发,中间不休息,到达另一个城市,所有可能花费的路费中最多是多少呢?

输入格式
输入的第一行包含一个整数 n,表示包括首都在内的T王国的城市数。

城市从 1 开始依次编号,1 号城市为首都。

接下来 n−1 行,描述T国的高速路(T国的高速路一定是 n−1 条)。

每行三个整数 Pi,Qi,Di,表示城市 Pi 和城市 Qi 之间有一条双向高速路,长度为 Di 千米。

输出格式
输出一个整数,表示大臣J最多花费的路费是多少。

数据范围
1≤n≤ 1 0 5 10^5 105,
1≤Pi,Qi≤n,
1≤Di≤1000
输入样例:
5
1 2 2
1 3 1
2 4 5
2 5 4
输出样例:
135
在这里插入图片描述

826. 单链表

7.1 贪心

1055. 股票买卖 II

给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

输入格式
第一行包含整数 N,表示数组长度。

第二行包含 N 个不大于 10000 的正整数,表示完整的数组。

输出格式
输出一个整数,表示最大利润。

数据范围
1≤N≤105
输入样例1:
6
7 1 5 3 6 4
输出样例1:
7
输入样例2:
5
1 2 3 4 5
输出样例2:
4
输入样例3:
5
7 6 4 3 1
输出样例3:
0
样例解释
样例1:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。共得利润 4+3 = 7。

样例2:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

样例3:在这种情况下, 不进行任何交易, 所以最大利润为 0。

贪心性质:①最优解 ②只看"眼前", 边枚举边选取 【难的是证明, 但是实在不会做就猜猜贪心是否对得上答案】

my分析

①最小的时候买入,最大的时候卖出,差值最大
②可能卖出后再次下降:重复① : 增减增减…
特殊:全部减:不赚钱,为0, 全部增最多赚maxv - minv
一般:在后续序列中找max差值, 卖出,然后再找max差值 a[j] - a[i]

贪心的做法很简单, 此题:后天 > 前天,交易一次
证明:【形如裂项相消法】贪心证明:中间项参与买和卖即有加有减:正负抵消
在这里插入图片描述



#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int price[N];

int main()
{
    scanf("%d", &n);
    for(int i= 0; i < n; i++) scanf("%d", &price[i]);
    
    int res = 0;
    for(int i = 0;i < n  - 1; i++)
    {
        int dt = price[i + 1] - price[i];//要学会减少代码量:把长且经常用的代码用短的变量替换
        if(dt > 0)
            res += dt;
    }
    
    printf("%d\n", res);
    
    return 0;
}

104. 货仓选址

在一条数轴上有 N 家商店,它们的坐标分别为 A1∼AN。

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

输入格式
第一行输入整数 N。

第二行 N 个整数 A1∼AN。

输出格式
输出一个整数,表示距离之和的最小值。

数据范围
1≤N≤100000,
0≤Ai≤40000
输入样例:
4
6 2 9 1
输出样例:
12

贪心猜想+时间复杂度规划+数学模型证明[简单题]
在这里插入图片描述

直觉:放在中间:res = ∑(maxv - minv) 【每轮依次选取第i大和第i小的坐标, 计算差值得距离累加到res】
时间复杂度:选取O(logn)算法:如排序
my证明思想:两点之间线段最短:最贪心:找最短不走多余的路,放在所有货仓的两点之间

常用证明技巧:找数学模型
奇数项就在中间项上,偶数项就在中间两项中间即在所有分组的区间中间,从几何意义距离角度看就是最小值

my_code:


#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;


typedef long long LL;
const int N = 100010;

int n;
int a[N];

int main()
{
    
    scanf("%d", &n);
    for(int i = 0; i < n; i++) scanf("%d", &a[i]);
    sort(a, a + n);
    
    LL res = 0;
    for(int i = 0; i < n / 2; i++)//每轮依次选取第i大和第i小的坐标, 计算差值得距离累加到res 
    {
        res += a[n - i - 1] - a[i];
    }
    
    printf("%lld\n", res);
    
    return 0;
}

y总:


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

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int x[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &x[i]);
    sort(x, x + n);

    int c = x[n / 2];//放在中间距离和最短
    LL res = 0;
    for (int i = 0; i < n; i ++ ) res += abs(x[i] - c);//计算距离和

    printf("%lld\n", res);

    return 0;
}

122. 糖果传递

有 n 个小朋友坐成一圈,每人有 a[i] 个糖果。

每人只能给左右两人传递糖果。

每人每次传递一个糖果代价为 1。

求使所有人获得均等糖果的最小代价。

输入格式
第一行输入一个正整数 n,表示小朋友的个数。

接下来 n 行,每行一个整数 a[i],表示第 i 个小朋友初始得到的糖果的颗数。

输出格式
输出一个整数,表示最小代价。

数据范围
1≤n≤1000000,
0≤a[i]≤ 2 × 1 0 9 2×10^9 2×109,
数据保证一定有解。

输入样例:
4
1
2
5
4
输出样例:
4

公式推导结论法
推导出的公式不是算法记忆主要滴, 属于能记住就记住, 记忆优先级较低
佬の链接

首先,最终每个小朋友的糖果数量可以计算出来,等于糖果总数除以n,用ave表示。
假设标号为i的小朋友开始有Ai颗糖果,Xi表示第i个小朋友给了第i-1个小朋友Xi颗糖果,如果Xi<0,说明第i-1个小朋友给了第i个小朋友Xi颗糖果,X1表示第一个小朋友给第n个小朋友的糖果数量。 所以最后的答案就是ans=|X1| + |X2| + |X3| + ……+ |Xn|。
对于第一个小朋友,他给了第n个小朋友X1颗糖果,还剩A1-X1颗糖果;但因为第2个小朋友给了他X2颗糖果,所以最后还剩A1-X1+X2颗糖果。根据题意,最后的糖果数量等于ave,即得到了一个方程:A1-X1+X2=ave。
同理,对于第2个小朋友,有A2-X2+X3=ave。最终,我们可以得到n个方程,一共有n个变量,但是因为从前n-1个方程可以推导出最后一个方程,所以实际上只有n-1个方程是有用的。
尽管无法直接解出答案,但可以用X1表示出其他的Xi,那么本题就变成了单变量的极值问题。
对于第1个小朋友,A1-X1+X2=ave -> X2=ave-A1+X1 = X1-C1(假设C1=A1-ave,下面类似)
对于第2个小朋友,A2-X2+X3=ave -> X3=ave-A2+X2=2ave-A1-A2+X1=X1-C2 即C2=A1+A2-2ave=A2+C1-ave以此类推
对于第3个小朋友,A3-X3+X4=ave -> X4=ave-A3+X3=3ave-A1-A2-A3+X1=X1-C3
我们的Ci的通式为Ci=Ai+C[i-1]-ave
……
对于第n个小朋友,An-Xn+X1=ave。
我们希望Xi的绝对值之和尽量小,即|X1| + |X1-C1| + |X1-C2| + ……+ |X1-Cn-1|要尽量小。注意到|X1-Ci|的几何意义是数轴上的点X1到Ci的距离,所以问题变成了:给定数轴上的n个点,找出一个到他们的距离之和尽量小的点,而这个点就是这些数中的中位数,证明略。


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

using namespace std;

typedef long long LL;

const int N = 1000010;

int n;
int a[N];
LL c[N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    LL sum = 0;
    for (int i = 1; i <= n; i ++ ) sum += a[i];

    LL avg = sum / n;
    for (int i = n; i > 1; i -- )
    {
        c[i] = c[i + 1] + avg - a[i];
    }
    c[1] = 0;

    sort(c + 1, c + n + 1);

    LL res = 0;
    for (int i = 1; i <= n; i ++ ) res += abs(c[i] - c[(n + 1) / 2]);

    printf("%lld\n", res);

    return 0;
}

112. 雷达设备

假设海岸是一条无限长的直线,陆地位于海岸的一侧,海洋位于另外一侧。

每个小岛都位于海洋一侧的某个点上。

雷达装置均位于海岸线上,且雷达的监测范围为 d,当小岛与某雷达的距离不超过 d 时,该小岛可以被雷达覆盖。

我们使用笛卡尔坐标系,定义海岸线为 x 轴,海的一侧在 x 轴上方,陆地一侧在 x 轴下方。

现在给出每个小岛的具体坐标以及雷达的检测范围,请你求出能够使所有小岛都被雷达覆盖所需的最小雷达数目。

输入格式
第一行输入两个整数 n 和 d,分别代表小岛数目和雷达检测范围。

接下来 n 行,每行输入两个整数,分别代表小岛的 x,y 轴坐标。

同一行数据之间用空格隔开。

输出格式
输出一个整数,代表所需的最小雷达数目,若没有解决方案则所需数目输出 −1。

数据范围
1≤n≤1000,
−1000≤x,y≤1000
输入样例:
3 2
1 2
-3 1
2 1
输出样例:
2

y总题解: 区间合并

(贪心) O(nlogn)
如下图所示,对于任意一个小岛 (x,y),我们都可以在海岸线上求出能覆盖该小岛的建造雷达的区间 [a,b]。
在这里插入图片描述

将所有小岛转化成区间后,问题转化为:给定 n 个区间,在 x 轴上选择尽量少的点,使得所有区间至少包含一个点。
在这里插入图片描述

将所有区间按右端点从小到大排序;[此题右端点比左端点好一些]
依次考虑每个区间:
1.如果当前区间包含最后一个选择的点,则直接跳过;
2.如果当前区间不包含最后一个选择的点,则在当前区间的右端点的位置选一个新的点;

计算每个坐标所对应的区间,需要 O(n)的计算量; d = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 d = \sqrt{(x1-x2)^2 + (y1 - y2)^2} d=(x1x2)2+(y1y2)2
将所有区间排序需要 O(nlogn) 的计算量;sort(起始, 起始 + 区间个数) 区间个数 = 坐标数量(转化成区间)
扫描所有区间需要 O(n) 的计算量;
所以总共的时间复杂度是 O(nlogn)。

按右端点排序 second排序优先 : struct 【或者first存y,但是书写是会影响逻辑,写代码别为难自己】

转化区间合并

#include<iostream>
#include<algorithm>
#include<cmath>

using namespace std;

const int N  = 1010;

int n, d;

struct Segment
{
    double l, r;//存每个点转换成区间的左右端点 【开根号注意double类型】
    bool operator< (const Segment& t) const     // &Segment: forbids declaration of 'Segment' with no type [-fpermissive]
    {
        return r < t.r;//按右端点从小到大排序
    }
}seg[N];

int main()
{
    scanf("%d%d", &n, &d);
    
    bool success = true;
    for(int i = 0; i < n; i++)
    {
        int x, y;//读取为整型
        scanf("%d%d", &x, &y);
        if(y > d)
        {
            success = false;//小岛离海岸线纵向距离y  > 海岸线上雷达纵向探测max_d : 无法探测
            break;
        }
        
        else
        {
            double len = sqrt(d * d - y * y);//开根号注意类型!!!
            seg[i] = {x - len, x + len};//seg[i].l = x - len, seg[i].r = x + len;
        }
    }
    
    if(!success) puts("-1");//或者直接输出-1
    else
    {
         sort(seg, seg + n);//区间个数 = 点个数 :按右端点从小到大排序
    
        int cnt = 0;//设置雷达的数量 == 选取区间数量
        double last = -1e20;//初始没有坐标,取-INF
        for(int i = 0; i < n; i++)
            if(last < seg[i].l)//下一个右 < 当前左端点 : 没有包含下一个区间的右端点,合并更新, 加入点
            {
                cnt ++;//需设置一个雷达
                last = seg[i].r;//更新, 以右端点排序, 贪心选取最右端点
            }
        printf("%d", cnt);
    }
    
    return 0;
}

不用success, 剪枝优化


#include<iostream>
#include<algorithm>
#include<cmath>

using namespace std;

const int N  = 1010;

int n, d;

struct Segment
{
    double l, r;//存每个点转换成区间的左右端点 【开根号注意double类型】
    bool operator< (const Segment& t) const     // &Segment: forbids declaration of 'Segment' with no type [-fpermissive]
    {
        return r < t.r;//按右端点从小到大排序
    }
}seg[N];

int main()
{
    scanf("%d%d", &n, &d);
    
    for(int i = 0; i < n; i++)
    {
        int x, y;//读取为整型
        scanf("%d%d", &x, &y);
        if(y > d)
        {
           puts("-1");//小岛离海岸线纵向距离y  > 海岸线上雷达纵向探测max_d : 无法探测
            return 0;
        }
        else
        {
            double len = sqrt(d * d - y * y);//开根号注意类型!!!
            seg[i] = {x - len, x + len};//seg[i].l = x - len, seg[i].r = x + len;
        }
    }
    
   
    sort(seg, seg + n);//区间个数 = 点个数 :按右端点从小到大排序

    int cnt = 0;//设置雷达的数量 == 选取区间数量
    double last = -1e20;//初始没有坐标,取-INF
    for(int i = 0; i < n; i++)
        if(last < seg[i].l)//下一个右 < 当前左端点 : 没有包含下一个区间的右端点,合并更新, 加入点
        {
            cnt ++;//需设置一个雷达
            last = seg[i].r;//更新, 以右端点排序, 贪心选取最右端点
        }
    printf("%d", cnt);
    
    return 0;
}

这是按左边界排序的吧
在这里插入图片描述


#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

#define x first
#define y second

using namespace std;

typedef pair<double, double> PDD;
const int N = 1010;
const double eps = 1e-6, INF = 1e10;  // double 存在精度差 ...

int n, d;  
PDD seg[N];//segment线段 , section区间hh

int main()
{
    cin >> n >> d;

    bool success = true;//判断是否能完全探测

    for (int i = 0; i < n; i ++ )
    {
        int x, y;
        scanf("%d%d", &x, &y);
        if (y > d)//小岛离海岸线纵向距离y  > 海岸线上雷达纵向探测max_d : 无法探测
        {
            success = false;
            break;
        }
        auto len = sqrt(d * d - y * y);//勾股定理
        seg[i] = {x + len, x - len};//存区间左右端点
    }

    if (!success) puts("-1");//有无法探测的小岛
    else
    {
        sort(seg, seg + n);//从小到大排序是按左端点。。。
        int res = 0;
        double last = -INF;
        for (int i = 0; i < n; i ++ )
        {
            if (seg[i].y > last + eps)//如果当前区间包含最后一个选择的点,则直接跳过;
            {
                res ++ ;
                last = seg[i].x;
            }
        }
        cout << res << endl;
    }
    return 0;
}
 
 

7.2 贪心——习题课

1235. 付账问题

几个人一起出去吃饭是常有的事。

但在结帐的时候,常常会出现一些争执。

现在有 n 个人出去吃饭,他们总共消费了 S 元。

其中第 i 个人带了 ai 元。

幸运的是,所有人带的钱的总数是足够付账的,但现在问题来了:每个人分别要出多少钱呢?

为了公平起见,我们希望在总付钱量恰好为 S 的前提下,最后每个人付的钱的标准差最小。

这里我们约定,每个人支付的钱数可以是任意非负实数,即可以不是 1 分钱的整数倍。

你需要输出最小的标准差是多少。

标准差的介绍:标准差是多个数与它们平均数差值的平方平均数,一般用于刻画这些数之间的“偏差有多大”。

形式化地说,设第 i 个人付的钱为 bi 元,那么标准差为 :

s = 1 n ∑ i = 1 n ( b i − 1 n ∑ i = 1 n b i ) 2 {s=\sqrt{\frac{1}{n}{\sum_{i=1}^n}\left({{b_i-\frac{1}{n}{\sum_{i=1}^n}b_i}}\right)^2}} s=n1i=1n(bin1i=1nbi)2

输入格式
第一行包含两个整数 n、S;

第二行包含 n 个非负整数 a1, …, an。

输出格式
输出最小的标准差,四舍五入保留 4 位小数。

数据范围
1≤n≤ 5 × 1 0 5 5×10^5 5×105,
0≤ai≤ 1 0 9 10^9 109,
0≤S≤ 1 0 15 10^{15} 1015

输入样例1:
5 2333
666 666 666 666 666
输出样例1:
0.0000
输入样例2:
10 30
2 1 4 7 4 8 3 6 4 7
输出样例2:
0.7928

1239. 乘积最大

给定 N 个整数 A1,A2,…AN。

请你从中选出 K 个数,使其乘积最大。

请你求出最大的乘积,由于乘积可能超出整型范围,你只需输出乘积除以 1000000009 的余数。

注意,如果 X<0, 我们定义 X 除以 1000000009 的余数是负(−X)除以 1000000009 的余数,即:0−((0−x)%1000000009)
输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行一个整数 Ai。

输出格式
输出一个整数,表示答案。

数据范围
1≤K≤N≤ 1 0 5 10^5 105,
− 1 0 5 −10^5 105≤Ai≤ 1 0 5 10^5 105
输入样例1:
5 3
-100000
-10000
2
100000
10000
输出样例1:
999100009
输入样例2:
5 3
-100000
-100000
-2
-100000
-100000
输出样例2:
-999999829

1247. 后缀表达式

给定 N 个加号、M 个减号以及 N+M+1 个整数 A1,A2,⋅⋅⋅,AN+M+1,小明想知道在所有由这 N 个加号、M 个减号以及 N+M+1 个整数凑出的合法的后缀表达式中,结果最大的是哪一个?

请你输出这个最大的结果。

例如使用 123+−,则 “23+1−” 这个后缀表达式结果是 4,是最大的。

输入格式
第一行包含两个整数 N 和 M。

第二行包含 N+M+1 个整数 A1,A2,⋅⋅⋅,AN+M+1。

输出格式
输出一个整数,代表答案。

数据范围
0 ≤ N , M ≤ 1 0 5 0≤N,M≤10^5 0N,M105,
− 1 0 9 ≤ A i ≤ 1 0 9 −10^9≤Ai≤10^9 109Ai109
输入样例:
1 1
1 2 3
输出样例:
4

1248. 灵能传输

在游戏《星际争霸 II》中,高阶圣堂武士作为星灵的重要 AOE 单位,在游戏的中后期发挥着重要的作用,其技能”灵能风暴“可以消耗大量的灵能对一片区域内的敌军造成毁灭性的伤害。

经常用于对抗人类的生化部队和虫族的刺蛇飞龙等低血量单位。

你控制着 n 名高阶圣堂武士,方便起见标为 1,2,⋅⋅⋅,n。

每名高阶圣堂武士需要一定的灵能来战斗,每个人有一个灵能值 a i a_i ai 表示其拥有的灵能的多少( a i a_i ai 非负表示这名高阶圣堂武士比在最佳状态下多余了 a i a_i ai点灵能, a i a_i ai为负则表示这名高阶圣堂武士还需要 − a i −a_i ai点灵能才能到达最佳战斗状态)。

现在系统赋予了你的高阶圣堂武士一个能力,传递灵能,每次你可以选择一个 i∈[2,n−1],若 a i ≥ 0 a_i≥0 ai0 则其两旁的高阶圣堂武士,也就是 i−1、i+1 这两名高阶圣堂武士会从 i 这名高阶圣堂武士这里各抽取 a i a_i ai 点灵能;若 a i < 0 a_i<0 ai<0 则其两旁的高阶圣堂武士,也就是 i−1,i+1 这两名高阶圣堂武士会给 i 这名高阶圣堂武士 − a i −a_i ai 点灵能。

形式化来讲就是 a i − 1 + = a i , a i + 1 + = a i , a i − = 2 a i a_{i−1}+=a_{i},a_{i+1}+=a_{i},a_{i}−=2a_{i} ai1+=ai,ai+1+=ai,ai=2ai

灵能是非常高效的作战工具,同时也非常危险且不稳定,一位高阶圣堂武士拥有的灵能过多或者过少都不好,定义一组高阶圣堂武士的不稳定度为 maxni=1|ai|,请你通过不限次数的传递灵能操作使得你控制的这一组高阶圣堂武士的不稳定度最小。

输入格式
本题包含多组询问。输入的第一行包含一个正整数 T 表示询问组数。

接下来依次输入每一组询问。

每组询问的第一行包含一个正整数 n,表示高阶圣堂武士的数量。

接下来一行包含 n 个数 a1,a2,⋅⋅⋅,an。

输出格式
输出 T 行。

每行一个整数依次表示每组询问的答案。

数据范围
1 ≤ T ≤ 3 , 3 ≤ n ≤ 300000 , ∣ a i ∣ ≤ 1 0 9 1≤T≤3,3≤n≤300000,|ai|≤10^9 1T3,3n300000,ai109,
每个评测用例的限制如下:

在这里插入图片描述

输入样例1:
3
3
5 -2 3
4
0 0 0 0
3
1 2 3
输出样例1:
3
0
3
输入样例2:
3
4
-1 -2 -3 7
4
2 3 4 -8
5
-1 -1 6 -1 -1
输出样例2:
5
7
4
样例解释
样例一
对于第一组询问:
对 2 号高阶圣堂武士进行传输操作后 a1=3,a2=2,a3=1。答案为 3。
对于第二组询问:
这一组高阶圣堂武士拥有的灵能都正好可以让他们达到最佳战斗状态。

8.1 数论

1246. 等差数列

数学老师给小明出了一道等差数列求和的题目。

但是粗心的小明忘记了一部分的数列,只记得其中 N 个整数。

现在给出这 N 个整数,小明想知道包含这 N 个整数的最短的等差数列有几项?

输入格式
输入的第一行包含一个整数 N。

第二行包含 N 个整数 A1,A2,⋅⋅⋅,AN。(注意 A1∼AN 并不一定是按等差数
列中的顺序给出)

输出格式
输出一个整数表示答案。

数据范围
2≤N≤100000,
0≤Ai≤ 1 0 9 10^9 109
输入样例:
5
2 6 4 10 20
输出样例:
10
样例解释
包含 2、6、4、10、20 的最短的等差数列是 2、4、6、8、10、12、14、16、18、20。
在这里插入图片描述

等差数列每项可表示为首项a[0] + k * d

特判所有数相等d为0,则长度就为n,直接输出n
每一项都是a加上k倍数公差d,每一项与第一项相减的最大公约数即为d, gcd(d, a[i] - a[0]) = gcd(d, kd) = d



#include <cstdio>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int a[N];

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    sort(a, a + n);

    int d = 0;
    for (int i = 1; i < n; i ++ ) d = __gcd(d, a[i] - a[0]); //algorithm库函数 __gcd

    if (!d) printf("%d\n", n);//d为0, 所有数相等
    else printf("%d\n", (a[n - 1] - a[0]) / d + 1);

    return 0;
}

1295. X的因子链

输入正整数 X,求 X 的大于 1 的因子组成的满足任意前一项都能整除后一项的严格递增序列的最大长度,以及满足最大长度的序列的个数。

输入格式
输入包含多组数据,每组数据占一行,包含一个正整数表示 X。

输出格式
对于每组数据,输出序列的最大长度以及满足最大长度的序列的个数。

每个结果占一行。

数据范围
1 ≤ X ≤ 2 20 1≤X≤2^{20} 1X220
输入样例:
2
3
4
10
100
输出样例:
1 1
1 1
2 1
2 2
4 6

1296. 聪明的燕姿

城市中人们总是拿着号码牌,不停寻找,不断匹配,可是谁也不知道自己等的那个人是谁。

可是燕姿不一样,燕姿知道自己等的人是谁,因为燕姿数学学得好!

燕姿发现了一个神奇的算法:假设自己的号码牌上写着数字 S,那么自己等的人手上的号码牌数字的所有正约数之和必定等于 S。

所以燕姿总是拿着号码牌在地铁和人海找数字(喂!这样真的靠谱吗)。

可是她忙着唱《绿光》,想拜托你写一个程序能够快速地找到所有自己等的人。

输入格式
输入包含 k 组数据。

对于每组数据,输入包含一个号码牌 S。

输出格式
对于每组数据,输出有两行。

第一行包含一个整数 m,表示有 m 个等的人。

第二行包含相应的 m 个数,表示所有等的人的号码牌。

注意:你输出的号码牌必须按照升序排列。

数据范围
1 ≤ k ≤ 100 , 1≤k≤100, 1k100,
1 ≤ S ≤ 2 × 1 0 9 1≤S≤2×10^9 1S2×109
输入样例:
42
输出样例:
3
20 26 41

1299. 五指山

大圣在佛祖的手掌中。

我们假设佛祖的手掌是一个圆圈,圆圈的长为 n,逆时针记为:0,1,2,…,n−1,而大圣每次飞的距离为 d。

现在大圣所在的位置记为 x,而大圣想去的地方在 y。

要你告诉大圣至少要飞多少次才能到达目的地。

注意:孙悟空的筋斗云只沿着逆时针方向翻。

输入格式
有多组测试数据。

第一行是一个正整数 T,表示测试数据的组数;

每组测试数据包括一行,四个非负整数,分别为如来手掌圆圈的长度 n,筋斗所能飞的距离 d,大圣的初始位置 x 和大圣想去的地方 y。

输出格式
对于每组测试数据,输出一行,给出大圣最少要翻多少个筋斗云才能到达目的地。

如果无论翻多少个筋斗云也不能到达,输出 Impossible。

数据范围
1≤T≤5,
2<n<109,
0<d<n,
0≤x,y<n
输入样例:
2
3 2 0 2
3 2 0 1
输出样例:
1
2

8.2 数论——习题课

1223. 最大比例

X星球的某个大奖赛设了 M 级奖励。

每个级别的奖金是一个正整数。

并且,相邻的两个级别间的比例是个固定值。

也就是说:所有级别的奖金数构成了一个等比数列。

比如:16,24,36,54,其等比值为:3/2。

现在,我们随机调查了一些获奖者的奖金数。

请你据此推算可能的最大的等比值。

输入格式
第一行为数字 N ,表示接下的一行包含 N 个正整数。

第二行 N 个正整数 Xi,用空格分开,每个整数表示调查到的某人的奖金数额。

输出格式
一个形如 A/B 的分数,要求 A、B 互质,表示可能的最大比例系数。

数据范围
0<N<100
0<Xi<1012
数据保证一定有解。

输入样例1:
3
1250 200 32
输出样例1:
25/4
输入样例2:
4
3125 32 32 200
输出样例2:
5/2
输入样例3:
3
549755813888 524288 2
输出样例3:
4/1

1301. C 循环

对于 C 语言的循环语句,形如:

for (variable = A; variable != B; variable += C)
  statement;

请问在 k 位存储系统中循环几次才会结束。

若在有限次内结束,则输出循环次数。否则输出死循环。

输入格式
多组数据,每组数据一行四个整数 A,B,C,k。

读入以 0 0 0 0 结束。

输出格式
若在有限次内结束,则输出循环次数。

否则输出 FOREVER。

数据范围
1≤k≤32,
0≤A,B,C< 2 k 2^k 2k
输入样例:
3 3 2 16
3 7 2 16
7 3 2 16
3 4 2 16
0 0 0 0
输出样例:
0
2
32766
FOREVER

1225. 正则问题

考虑一种简单的正则表达式:

只由 x ( ) | 组成的正则表达式。

小明想求出这个正则表达式能接受的最长字符串的长度。

例如 ((xx|xxx)x|(x|xx))xx 能接受的最长字符串是: xxxxxx,长度是6。

输入格式
一个由x()|组成的正则表达式。

输出格式
输出所给正则表达式能接受的最长字符串的长度。

数据范围
输入长度不超过100,保证合法。

输入样例:
((xx|xxx)x|(x|xx))xx
输出样例:
6

1243. 糖果

糖果店的老板一共有 M 种口味的糖果出售。

为了方便描述,我们将 M 种口味编号 1∼M。

小明希望能品尝到所有口味的糖果。

遗憾的是老板并不单独出售糖果,而是 K 颗一包整包出售。

幸好糖果包装上注明了其中 K 颗糖果的口味,所以小明可以在买之前就知道每包内的糖果口味。

给定 N 包糖果,请你计算小明最少买几包,就可以品尝到所有口味的糖果。

输入格式
第一行包含三个整数 N,M,K。

接下来 N 行每行 K 个整数 T1,T2,⋅⋅⋅,TK,代表一包糖果的口味。

输出格式
一个整数表示答案。

如果小明无法品尝所有口味,输出 −1。

数据范围
1≤N≤100,
1≤M,K≤20,
1≤Ti≤M
输入样例:
6 5 3
1 1 2
1 2 3
1 1 3
2 3 5
5 4 2
5 1 2
输出样例:
2

9.1 复杂DP

1050. 鸣人的影分身

在火影忍者的世界里,令敌人捉摸不透是非常关键的。

我们的主角漩涡鸣人所拥有的一个招数——多重影分身之术——就是一个很好的例子。

影分身是由鸣人身体的查克拉能量制造的,使用的查克拉越多,制造出的影分身越强。

针对不同的作战情况,鸣人可以选择制造出各种强度的影分身,有的用来佯攻,有的用来发起致命一击。

那么问题来了,假设鸣人的查克拉能量为 M,他影分身的个数最多为 N,那么制造影分身时有多少种不同的分配方法?

注意:

影分身可以分配0点能量。
分配方案不考虑顺序,例如:M=7,N=3,那么 (2,2,3) 和 (2,3,2) 被视为同一种方案。
输入格式
第一行是测试数据的数目 t。

以下每行均包含二个整数 M 和 N,以空格分开。

输出格式
对输入的每组数据 M 和 N,用一行输出分配的方法数。

数据范围
0≤t≤20,
1≤M,N≤10
输入样例:
1
7 3
输出样例:
8

1047. 糖果

由于在维护世界和平的事务中做出巨大贡献,Dzx被赠予糖果公司2010年5月23日当天无限量糖果免费优惠券。

在这一天,Dzx可以从糖果公司的 N 件产品中任意选择若干件带回家享用。

糖果公司的 N 件产品每件都包含数量不同的糖果。

Dzx希望他选择的产品包含的糖果总数是 K 的整数倍,这样他才能平均地将糖果分给帮助他维护世界和平的伙伴们。

当然,在满足这一条件的基础上,糖果总数越多越好。

Dzx最多能带走多少糖果呢?

注意:Dzx只能将糖果公司的产品整件带走。

输入格式
第一行包含两个整数 N 和 K。

以下 N 行每行 1 个整数,表示糖果公司该件产品中包含的糖果数目,不超过 1000000。

输出格式
符合要求的最多能达到的糖果总数,如果不能达到 K 的倍数这一要求,输出 0。

数据范围
1≤N≤100,
1≤K≤100,

输入样例:
5 7
1
2
3
4
5
输出样例:
14
样例解释
Dzx的选择是2+3+4+5=14,这样糖果总数是7的倍数,并且是总数最多的选择。

1222. 密码脱落

X星球的考古学家发现了一批古代留下来的密码。

这些密码是由A、B、C、D 四种植物的种子串成的序列。

仔细分析发现,这些密码串当初应该是前后对称的(也就是我们说的镜像串)。

由于年代久远,其中许多种子脱落了,因而可能会失去镜像的特征。

你的任务是:

给定一个现在看到的密码串,计算一下从当初的状态,它要至少脱落多少个种子,才可能会变成现在的样子。

输入格式
共一行,包含一个由大写字母ABCD构成的字符串,表示现在看到的密码串。

输出格式
输出一个整数,表示至少脱落了多少个种子。

数据范围
输入字符串长度不超过1000

输入样例1:
ABCBA
输出样例1:
0
输入样例2:
ABDCDCBABC
输出样例2:
3

给定字符串,最少删去多少个字符,变成回文字符串

从当前样子变成初始状态需要添加叶子的数量 等价于 当前样子变成最大的回文串需要剪去的叶子的数量
即至少脱落多少个种子 等价于 总数量 - 最大回文子序列的长度

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

char s[N];
int f[N][N];

int main()
{
    scanf("%s", s);
    int n = strlen(s);

    for (int len = 1; len <= n; len ++ )
        for (int l = 0; l + len - 1 < n; l ++ )
        {
            int r = l + len - 1;
            if (len == 1) f[l][r] = 1;
            else
            {
                if (s[l] == s[r]) f[l][r] = f[l + 1][r - 1] + 2; //左右端点相等
                f[l][r] = max(f[l][r], max(f[l + 1][r] ,f[l][r - 1])); //其余两种状态,添加单点得到回文  
            }
        }

    printf("%d\n", n - f[0][n - 1]);

    return 0;
}
#include <cstdio>
#include <string.h>

const int N = 1010;

char s[N];
int f[N][N];

int main()
{
    scanf("%s", s);
    int n = strlen(s);

    for (int len = 1; len <= n; len ++ )
        for (int l = 0; l + len - 1 < n; l ++ )
        {
            int r = l + len - 1;
            if (len == 1) f[l][r] = 1;
            else
            {
                if (s[l] == s[r]) f[l][r] = f[l + 1][r - 1] + 2; //左右端点相等
                if (f[l][r - 1] > f[l][r]) f[l][r] = f[l][r - 1];  //其余两种状态,添加单点得到回文 
                if (f[l + 1][r] > f[l][r]) f[l][r] = f[l + 1][r]; //
            }
        }

    printf("%d\n", n - f[0][n - 1]);

    return 0;
}

1220. 生命之树

在X森林里,上帝创建了生命之树。

他给每棵树的每个节点(叶子也称为一个节点)上,都标了一个整数,代表这个点的和谐值。

上帝要在这棵树内选出一个非空节点集 S,使得对于 S 中的任意两个点 a,b,都存在一个点列 {a,v1,v2,…,vk,b} 使得这个点列中的每个点都是 S 里面的元素,且序列中相邻两个点间有一条边相连。

在这个前提下,上帝要使得 S 中的点所对应的整数的和尽量大。

这个最大的和就是上帝给生命之树的评分。

经过 atm 的努力,他已经知道了上帝给每棵树上每个节点上的整数。

但是由于 atm 不擅长计算,他不知道怎样有效的求评分。

他需要你为他写一个程序来计算一棵树的分数。

输入格式
第一行一个整数 n 表示这棵树有 n 个节点。

第二行 n 个整数,依次表示每个节点的评分。

接下来 n−1 行,每行 2 个整数 u,v,表示存在一条 u 到 v 的边。

由于这是一棵树,所以是不存在环的。

树的节点编号从 1 到 n。

输出格式
输出一行一个数,表示上帝给这棵树的分数。

数据范围
1≤n≤105,
每个节点的评分的绝对值均不超过 106。

输入样例:
5
1 -2 -3 4 5
4 2
3 1
1 2
2 5
输出样例:
8

1303. 斐波那契前 n 项和

大家都知道 Fibonacci 数列吧, f 1 = 1 , f 2 = 1 , f 3 = 2 , f 4 = 3 , … , f n = f n − 1 + f n − 2 f_1=1,f_2=1,f_3=2,f_4=3,…,f_n=f_{n−1}+f_{n−2} f1=1,f2=1,f3=2,f4=3,,fn=fn1+fn2

现在问题很简单,输入 n 和 m,求 fn 的前 n 项和 S n S_n Sn mod m。

输入格式
共一行,包含两个整数 n 和 m。

输出格式
输出前 n 项和 Snmodm 的值。

数据范围
1≤n≤2000000000,
1≤m≤1000000010
输入样例:
5 1000
输出样例:
12

9.2 复杂DP——习题课

1226. 包子凑数

小明几乎每天早晨都会在一家包子铺吃早餐。

他发现这家包子铺有 N 种蒸笼,其中第 i 种蒸笼恰好能放 Ai 个包子。

每种蒸笼都有非常多笼,可以认为是无限笼。

每当有顾客想买 X 个包子,卖包子的大叔就会迅速选出若干笼包子来,使得这若干笼中恰好一共有 X 个包子。

比如一共有 3 种蒸笼,分别能放 3、4 和 5 个包子。

当顾客想买 11 个包子时,大叔就会选 2 笼 3 个的再加 1 笼 5 个的(也可能选出 1 笼 3 个的再加 2 笼 4 个的)。

当然有时包子大叔无论如何也凑不出顾客想买的数量。

比如一共有 3 种蒸笼,分别能放 4、5 和 6 个包子。

而顾客想买 7 个包子时,大叔就凑不出来了。

小明想知道一共有多少种数目是包子大叔凑不出来的。

输入格式
第一行包含一个整数 N。

接下来 N 行,每行包含一个整数 Ai。

输出格式
输出一个整数代表答案。

如果凑不出的数目有无限多个,输出INF。

数据范围
1≤N≤100,
1≤Ai≤100
输入样例1:
2
4
5
输出样例1:
6
输入样例2:
2
4
6
输出样例2:
INF
样例解释
对于样例1,凑不出的数目包括:1, 2, 3, 6, 7, 11。
对于样例2,所有奇数都凑不出来,所以有无限多个。

1070. 括号配对

Hecy 又接了个新任务:BE 处理。

BE 中有一类被称为 GBE。

以下是 GBE 的定义:

空表达式是 GBE
如果表达式 A 是 GBE,则 [A] 与 (A) 都是 GBE
如果 A 与 B 都是 GBE,那么 AB 是 GBE
下面给出一个 BE,求至少添加多少字符能使这个 BE 成为 GBE。

注意:BE 是一个仅由(、)、[、]四种字符中的若干种构成的字符串。

输入格式
输入仅一行,为字符串 BE。

输出格式
输出仅一个整数,表示增加的最少字符数。

数据范围
对于所有输入字符串,其长度小于100。

输入样例:
[])
输出样例:
1

1078. 旅游规划

W 市的交通规划出现了重大问题,市政府下定决心在全市各大交通路口安排疏导员来疏导密集的车流。

但由于人员不足,W 市市长决定只在最需要安排人员的路口安排人员。

具体来说,W 市的交通网络十分简单,由 n 个交叉路口和 n−1 条街道构成,交叉路口路口编号依次为 0,1,…,n−1 。

任意一条街道连接两个交叉路口,且任意两个交叉路口间都存在一条路径互相连接。

经过长期调查,结果显示,如果一个交叉路口位于 W 市交通网最长路径上,那么这个路口必定拥挤不堪。

所谓最长路径,定义为某条路径 p=(v1,v2,…,vk),路径经过的路口各不相同,且城市中不存在长度大于 k 的路径(因此最长路径可能不唯一)。

因此 W 市市长想知道哪些路口位于城市交通网的最长路径上。

输入格式
第一行包含一个整数 n。

之后 n−1 行每行两个整数 u,v,表示编号为 u 和 v 的路口间存在着一条街道。

输出格式
输出包括若干行,每行包括一个整数——某个位于最长路径上的路口编号。

为了确保解唯一,请将所有最长路径上的路口编号按编号顺序由小到大依次输出。

数据范围
1≤n≤2×105
输入样例:
10
0 1
0 2
0 4
0 6
0 7
1 3
2 5
4 8
6 9
输出样例:
0
1
2
3
4
5
6
8
9

1217. 垒骰子

赌圣atm晚年迷恋上了垒骰子,就是把骰子一个垒在另一个上边,不能歪歪扭扭,要垒成方柱体。

经过长期观察,atm 发现了稳定骰子的奥秘:有些数字的面贴着会互相排斥!

我们先来规范一下骰子:1 的对面是 4,2 的对面是 5,3 的对面是 6。

假设有 m 组互斥现象,每组中的那两个数字的面紧贴在一起,骰子就不能稳定的垒起来。

atm想计算一下有多少种不同的可能的垒骰子方式。

两种垒骰子方式相同,当且仅当这两种方式中对应高度的骰子的对应数字的朝向都相同。

由于方案数可能过多,请输出模 1 0 9 + 7 10^9+7 109+7 的结果。

输入格式
第一行包含两个整数 n,m,分别表示骰子的数目和排斥的组数。

接下来 m 行,每行两个整数 a,b,表示 a 和 b 数字不能紧贴在一起。

输出格式
共一个数,表示答案模 1 0 9 + 7 10^9+7 109+7 的结果。

数据范围
1≤n≤ 1 0 9 10^9 109,
1≤m≤36,
1≤a,b≤6
输入样例:
2 1
1 2
输出样例:
544

10.1 疑难杂题

1242. 修改数组

给定一个长度为 N 的数组 A=[A1,A2,⋅⋅⋅AN],数组中有可能有重复出现的整数。

现在小明要按以下方法将其修改为没有重复整数的数组。

小明会依次修改 A2,A3,⋅⋅⋅,AN。

当修改 Ai 时,小明会检查 Ai 是否在 A1∼Ai−1 中出现过。

如果出现过,则小明会给 Ai 加上 1;如果新的 Ai 仍在之前出现过,小明会持续给 Ai 加 1,直到 Ai 没有在 A1∼Ai−1 中出现过。

当 AN 也经过上述修改之后,显然 A 数组中就没有重复的整数了。

现在给定初始的 A 数组,请你计算出最终的 A 数组。

输入格式
第一行包含一个整数 N。

第二行包含 N 个整数 A1,A2,⋅⋅⋅,AN。

输出格式
输出 N 个整数,依次是最终的 A1,A2,⋅⋅⋅,AN。

数据范围
1≤N≤105,
1≤Ai≤106
输入样例:
5
2 1 1 3 4
输出样例:
2 1 3 4 5

1234. 倍数问题

众所周知,小葱同学擅长计算,尤其擅长计算一个数是否是另外一个数的倍数。

但小葱只擅长两个数的情况,当有很多个数之后就会比较苦恼。

现在小葱给了你 n 个数,希望你从这 n 个数中找到三个数,使得这三个数的和是 K 的倍数,且这个和最大。

数据保证一定有解。

输入格式
第一行包括 2 个正整数 n, K。

第二行 n 个正整数,代表给定的 n 个数。

输出格式
输出一行一个整数代表所求的和。

数据范围
1≤n≤105,
1≤K≤103,
给定的 n 个数均不超过 108
输入样例:
4 3
1 2 3 4
输出样例:
9

1213. 斐波那契

斐波那契数列大家都非常熟悉。它的定义是:

f(x)=1…(x=1,2)

f(x)=f(x−1)+f(x−2)…(x>2)
对于给定的整数 n 和 m,我们希望求出:

f(1)+f(2)+…+f(n) 的值。

但这个值可能非常大,所以我们把它对 f(m) 取模。

但这个数字依然很大,所以需要再对 p 求模。

输入格式
输入包含多组数据。

每组数据占一行,包含三个整数 n,m,p。

输出格式
每组数据输出一个整数,表示答案。

每个数占一行。

数据范围
0<n,m,p< 1 0 18 10^{18} 1018
测试数据不超过100组

输入样例1:
2 3 5
输出样例1:
0
输入样例2:
15 11 29
输出样例2:
25

1171. 距离

众所周知,小葱同学擅长计算,尤其擅长计算一个数是否是另外一个数的倍数。

但小葱只擅长两个数的情况,当有很多个数之后就会比较苦恼。

现在小葱给了你 n 个数,希望你从这 n 个数中找到三个数,使得这三个数的和是 K 的倍数,且这个和最大。

数据保证一定有解。

输入格式
第一行包括 2 个正整数 n, K。

第二行 n 个正整数,代表给定的 n 个数。

输出格式
输出一行一个整数代表所求的和。

数据范围
1≤n≤ 1 0 5 10^5 105,
1≤K≤ 1 0 3 10^3 103,
给定的 n 个数均不超过 108
输入样例:
4 3
1 2 3 4
输出样例:
9

10.2 疑难杂题——习题课

1206. 剪格子

给出 n 个点的一棵树,多次询问两点之间的最短距离。

注意:

边是无向的。
所有节点的编号是 1,2,…,n。
输入格式
第一行为两个整数 n 和 m。n 表示点数,m 表示询问次数;

下来 n−1 行,每行三个整数 x,y,k,表示点 x 和点 y 之间存在一条边长度为 k;

再接下来 m 行,每行两个整数 x,y,表示询问点 x 到点 y 的最短距离。

树中结点编号从 1 到 n。

输出格式
共 m 行,对于每次询问,输出一行询问结果。

数据范围
2 ≤ n ≤ 1 0 4 2≤n≤10^4 2n104,
1 ≤ m ≤ 2 × 1 0 4 1≤m≤2×10^4 1m2×104,
0 < k ≤ 100 0<k≤100 0<k100,
1 ≤ x , y ≤ n 1≤x,y≤n 1x,yn
输入样例1:
2 2
1 2 100
1 2
2 1
输出样例1:
100
100
输入样例2:
3 2
1 2 10
3 1 15
1 2
3 2
输出样例2:
10
25

523. 组合数问题

组合数 Cmn 表示的是从 n 个物品中选出 m 个物品的方案数。

举个例子,从 (1, 2, 3) 三个物品中选择两个物品可以有 (1, 2), (1, 3), (2, 3) 这三种选择方法。

根据组合数的定义,我们可以给出计算组合数  C n m C^m_n Cnm 的一般公式:

C n m = n ! m ! ( n − m ) ! C^m_n=\frac{n!}{m!(n−m)!} Cnm=m!(nm)!n!
其中 n! = 1 × 2 × ⋅ ⋅ ⋅ × n。

小葱想知道如果给定 n, m 和 k ,对于所有的 0 ≤ i ≤ n, 0 ≤ j ≤ min(i, m) 有多少对 (i, j) 满足 Cji 是 k 的倍数。

输入格式
第一行有两个整数 t, k ,其中 t 代表该测试点总共有多少组测试数据,k 的意义见问题描述。

接下来 t 行每行两个整数 n, m,其中 n, m 的意义见问题描述。

输出格式
共 t 行,每行一个整数代表所有的 0 ≤ i ≤ n, 0 ≤ j ≤ min(i, m) 有多少对 (i, j) 满足 Cji 是 k 的倍数。

数据范围
1≤n,m≤2000,2≤k≤21,1≤t≤ 1 0 4 10^4 104
输入样例:
1 2
3 3
输出样例:
1

840. 模拟散列表

维护一个集合,支持如下几种操作:

I x,插入一个数 x;
Q x,询问数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式
第一行包含整数 N,表示操作数量。

接下来 N 行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。

输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围
1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
− 1 0 9 ≤ x ≤ 1 0 9 −10^9≤x≤10^9 109x109
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No

11 第十一届蓝桥杯省赛C++B组真题(7月)

12 第十二届蓝桥杯省赛C++B组真题(4月)

3416. 时间显示

3417. 砝码称重

3418. 杨辉三角形

如果我们按从上到下、从左到右的顺序把所有数排成一列,可以得到如下数列:

1, 1, 1, 1, 2, 1, 1, 3, 3, 1, 1, 4, 6, 4, 1, …

给定一个正整数 N,请你输出数列中第一次出现 N
是在第几个数?

输入格式
输入一个整数 N。

输出格式
输出一个整数代表答案。

数据范围
对于 20% 的评测用例,1≤N≤10;
对于所有评测用例,1≤N≤ 1 0 9 10^9 109

输入样例:

6

输出样例:

13
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

int n;

LL C(int a, int b) //【不取模】
{
    LL res = 1;
    for (int i = a, j = 1; j <= b; i --, j ++ ) //O(n)
    {
        res = res * i / j;
        if (res > n) return res; //时间优化提前结束 - 不可能为答案
    }
    return res;
}

bool check(int k)
{
    LL l = k * 2, r = max((LL)n, l);
    while (l < r)
    {
        LL mid = l + r >> 1; //二分查找 第一个 >= n的位置   O(logn)
        if (C(mid, k) >= n) r = mid;
        else l = mid + 1;
    }
    if (C(r, k) != n) return false; //没有找到等于n的值

    cout << r * (r + 1) / 2 + k + 1 << endl;  //位置关系 前面的层数1, 2 ,3 ... r   + (k从0开始 + 1)

    return true;
}

int main()
{
    cin >> n;
    for (int k = 16; ; k -- ) //估算2^16    [n规定有解]
        if (check(k))
            break;
    return 0;
}
//  1  <=  N <= 1e9    组合数 C(a)(b) 不够
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 10, mod = 1e9;

int n;
int c[N][N];

void init()
{
    for(int i = 0; i < N; i++)
        for(int j = 0; j <= i; j++)
            if(!j) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod; //, printf("%d %d %d\n", i, j, c[i][j])
    
}

int main()  
{
    init();
    int x, y;
    scanf("%d", &n); //用组合数
    if(n == 1) 
    {
        puts("1");
        return 0;
    }
    for(int i = 0; i < N; i++)
        for(int j = 0; j <= i; j++)
            if(c[i][j] == n) 
            {
                x = i; y = j;  //下标0开始
                printf("%d %d\n", x, y);
                i = N;//【等效双层break;】
            }
    int k = x * (x + 1) / 2 + y + 1;// (1 + 2 + 3 ... x从0开始) + y从0开始 + 1
    printf("%d", k);
    
    return 0;
}
/* 求杨辉三角第n个数 */
// int k = 0, t = n, p = 1;
// while (n -- )
// {
//     k ++; //在杨辉三角第几层: 每层第一个数排序编号为2^n, 对应数值 C[k - 1][n - p]
//     n /= 2;    n = 2 1
//     p <<= 1;
// }
// int y = n - p; 

3419. 双向排序

3420. 括号序列

3421. 异或数列

3422. 左孩子右兄弟

3423. 分果果

3424. 最少砝码

13 第十三届蓝桥杯C++ B组真题

  • 34
    点赞
  • 377
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

violet~evergarden

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值