上机纸质资料

常用代码模板1——基础算法

快速排序算法模板

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;
    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

归并排序算法模板

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 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];
}

整数二分算法模板

二分问题:有单调性一定可以用二分法来做,没有也不一定不行

二分问题要处理好上溢和下溢,一种方法是设置哨兵,另一种方法是二分得到的下标对应的答案错位存储

//check的确定:
//二分点本质上是使命题p(x)成立的最大(或最小)的x
//则check函数就设置为p(x)成立  
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

浮点数二分算法模板

//误差经验值:
//误差为1e-x,x比要保留的小数点位数多2
bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

高精度加法

// C = A + B, A >= 0, B >= 0
//一般len(A)=1e6, len(B)=1e6
vector<int> add(vector<int> &A, vector<int> &B)
{
    if (A.size() < B.size()) return add(B, A);

    vector<int> C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(t);
    return C;
}

高精度减法

// C = A - B, 满足A >= B, A >= 0, B >= 0
//一般len(A)=1e6, len(B)=1e6
vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++ )
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10); //t>=0 res=t t<0, res=t+10; 合并得res=(t+10)%10;
        if (t < 0) t = 1;	//t<0 则借了1
        else t = 0;			//否则没有借位
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back(); //因为c的长度与a相同,可能有前导零,要去掉
    return C;
}

高精度乘低精度

// C = A * b, A >= 0, b >= 0
//一般len(A)<=1e6, a<=1e9
vector<int> mul(vector<int> &A, int b)
{
    vector<int> C;

    int t = 0;
    for (int i = 0; i < A.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();

    return C;
}

高精度乘高精度

vector<int> mul(vector<int> &a, vector<int> &b)
{
    vector<int> c(a.size() + b.size() + 1);
    int t = 0;
    for (int i = 0; i < a.size(); i++)
        for (int j = 0; j < b.size(); j++)
            c[i + j] += a[i] * b[j];
    for (int i = 0; i < c.size(); i++)
        c[i + 1] += c[i] / 10, c[i] %= 10;
    while (c.size() > 1 && c.back() == 0)
        c.pop_back();
    return c;
}

高精度除以低精度

// A / b = C ... r, A >= 0, b > 0
vector<int> div(vector<int> &A, int b, int &r)
{
    vector<int> C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i -- )
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

一维前缀和

S[i] = a[1] + a[2] + ... + a[i]
//a数组从下标1开始存,S[0]=0
S[i] = S[i-1] + a[i] //i从1开始。
//把数组变成自身的前缀和
S[i] += S[i-1]		//i从1开始
//求[l, r]的元素的和
a[l] + ... + a[r] = S[r] - S[l - 1]

二维前缀和

//a数组从(1,1)开始存
//S[i, j] = 第i行j列格子左上部分所有元素的和
S[i, j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + a[i][j]
//把数组变成自身的前缀和
S[i, j] += S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1]
//以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

一维差分

//数组从下标1开始存
//给区间[l, r]中的每个数加上c:
B[l] += c, B[r + 1] -= c

二维差分

//数组从(1,1)开始存
//给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

位运算

//求n的第k位数字: (从右往左由0开始数)
n >> k & 1
//返回n的最后一位1:
lowbit(n) = n & -n

离散化

//一种特殊的哈希方式,要求保序(需要先排序去重),find()单调 
//离散化处理的问题:下标范围有负数,或者下标范围超过1e6以上,下标个数小于下标范围,并且题目还要求按照下标的顺序做题

//离散化的本质就是不使用原来的绝对坐标,而是以每一个绝对坐标在所有坐标中的排位作为坐标使用,由于绝对坐标的范围大但个数少,就可以达到缩小坐标范围的效果
//注意:要一以贯之地使用一套坐标

vector<int> alls; // 存储所有待离散化的值

// 1.排序去重,为二分查找做准备
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

// 2.二分求出x对应的离散化的值
// 通过二分,确定x在所有坐标中的相对位置(排第几个),返回相对坐标(排位)
int find(int x) // 找到第一个大于等于x的位置
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 映射到1, 2, ...n,不加1则从下标0开始映射
}

区间合并

// 将所有存在交集的区间合并
//typedef pair<int, int> PII;

void merge(vector<PII> &segs)
{
    vector<PII> res;
    
	//pair中sort默认对first升序,当first相同时对second升序
    sort(segs.begin(), segs.end()); 

    int st = -2e9, ed = -2e9; //初始维护区间设为在负无穷的长度为0的区间
    //[st, ed]是当前维护的区间
    //遍历所有区间,最后维护的区间没存入res就会退出循环
    for (auto seg : segs)
        if (ed < seg.first) //当前扫描到的区间和维护的区间没有交集
        {
            if (st != -2e9) res.push_back({st, ed}); //如果维护的区间不是初始区间,则存入res
            st = seg.first, ed = seg.second;		//将维护的区间更换为当前区间
        }
        else ed = max(ed, seg.second);	//否则更新维护区间的右端点

    if (st != -2e9) res.push_back({st, ed}); //如果最后维护的区间不是初始区间,则存入res

    segs = res; 
}

等比数列前n项和

// 求 p^0 + p^1 + .. + p^(k - 1)
int sum(int p, int k)
{
    if (k == 1) return 1;
    if (k % 2 == 0) return (1 + qmi(p, k / 2)) * sum(p, k / 2) % mod;
    return (sum(p, k - 1) + qmi(p, k - 1)) % mod;
}

常用代码模板2——数据结构

双链表

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前可以用的最新的点的下标
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    //0,1相当于是虚节点,只起到标识链表的开始和结束的作用,不存值
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x(a是节点的idx值,假如节点是第k个插入的数,那a = k + 1)
// 在最左端插入:insert(0, x);
// 在最右端插入:insert(l[1], x);
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}
//打印: for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';

单调栈

// 先思考暴力做法,再挖掘单调性,考虑符合什么条件的元素是没用的,把它删掉
// 消除某种性质的逆序对
// 常见模型:找出每个数左边离它最近的比它大/小的数
// 步骤:弹栈(维护单调性)-> 新元素入栈
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ; 
    stk[ ++ tt] = i;
}

单调队列

// 先思考暴力做法,再挖掘单调性,考虑符合什么条件的元素是没用的,把它删掉
// 消除某种性质的逆序对
// 常见模型:找出滑动窗口中的最大值/最小值
// 步骤:队首出队(维护窗口长度)-> 队尾出队(维护单调性)-> 新元素入队
// 保证队首元素是我们想要的值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ; // hh用于维护队列长度, 所以要从队尾出队
    q[ ++ tt] = i;
}

并查集

1. 朴素并查集

// 处理的问题:1.将两个集合合并 2.询问两个元素是否在一个集合当中
// 时间复杂度:最坏O(nlogn)
// 基本原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号(即树根有p[x]==x)。每个节点储存它的父节点,p[x]表示x的父节点
int p[N]; //存储每个点的祖宗节点

// 返回x的祖宗节点 + 路径压缩
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]); // 路径压缩
    return p[x]; // 返回x的祖宗节点
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:(令a的祖宗节点的父节点为b的祖宗节点)
p[find(a)] = find(b);
// 判断两个点是否属于同一集合
if(find(a) == find(b)) {

}

2. 维护size的并查集

int p[N], cnt[N];
// p[]存储每个点的祖宗节点, cnt[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
    p[i] = i;
    cnt[i] = 1;
}
// 合并a和b所在的两个集合:
// if(find(a) ==  find(b)) 跳过,否则会size会自增
cnt[find(b)] += cnt[find(a)]; // 维护size
p[find(a)] = find(b);

3. 维护到祖宗节点距离的并查集

// 带权并查集精髓总结:只要两个元素在一个集合里面,通过它们与根节点的距离就能知道它们的相对关系。
int p[N], d[N];
// p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
    if (p[x] != x)
    {
        int u = find(p[x]);
        d[x] += d[p[x]];
        p[x] = u;
    }
    return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
    p[i] = i;
    d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

常用代码模板3——搜索与图论

拓扑排序

时间复杂度 O(n+m), n 表示点数,m 表示边数

// 递推的思想:如果要求一个图的拓扑序列,可以先找到一个入度为0的点并摘掉它,求剩余图的拓扑序列,再将该点放在开头即可得到原图的拓扑序列。最小问题(一个点就是它自身的拓扑序列)有解,
// 思路:先让所有入度为0的点入队,取队头并出队,遍历所有邻点,将由该点出发的边都删除,就相当于将这个点摘掉。然后在删的过程中,有邻点的入度减为零,则该邻点可以作为新图的起点,要入队。如果所有的点都入队了,说明每个新图都是有入度为0的点作为起点的(即有解),那么原问题一定有解,并且由于每个新图的起点都从队尾入队,则拓扑序列就存在队列中。
bool topsort()
{
    int hh = 0, tt = -1;

    // d[i] 存储点i的入度
    for (int i = 1; i <= n; i ++ )
        if (!d[i])
            q[ ++ tt] = i;

    while (hh <= tt)
    {
        int t = q[hh ++ ];

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (-- d[j] == 0)
                q[ ++ tt] = j;
        }
    }

    // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
    return tt == n - 1;
}

最短路算法

最短路问题

  1. 概念

    • 源点:起点
    • 汇点:终点
    • n:点数
    • m:边数
  2. 分类

    • 单源最短路:从一个固定点到其他所有点的最短距离(如从1->n)

      • 所有边权都是正数:
        1. 朴素dijkstra算法:复杂度\(O(n^2)\),与边数无关,适用于稠密图(点少边多),例如边数是\(n^2\)级别的。
        2. 堆优化版dijkstra算法:复杂度\(O(mlogn)\),适用于稀疏图(点和边一个级别或点多边少),例如点数和边数差不多时。
      • 存在负权边:
        1. bellman-ford算法:复杂度\(O(nm)\),一般用于求不超过k条边的最短路
        2. spfa算法:一般\(O(m)\),最坏\(O(nm)\),如果对最短路经过的边数限制,只能用bellman-ford算法
    • 多源汇最短路:求任意两个点之间的最短距离(起点不止一个)

      floyd算法:复杂度\(O(n^3)\)

  3. 关键:建图(将原问题抽象成最短路),如何定义点和边,不侧重于证明算法的正确性


dijkstra算法(无负权边)

1. 朴素dijkstra算法(稠密图)

时间复杂是 \(O(n^2)\), n 表示点数

步骤:(这里的距离指的都是到出发点的距离)

  1. 起点的距离置为0,其他点的距离置为\(\infty\)

  2. 每次从未标记的节点中选择距离最小的节点赋给 t ,并把这个点标记,收录到最短路径集合中(基于贪心算法,没有确定最短路的、距离出发点最近的点此时的距离就是它的最短距离)

  3. 用 t 更新邻点的距离,若(节点 t 的距离 + 节点 t 到该节点的边长)<该节点的距离,就更新该节点的距离

    由于 :

     1. 已经确定最短路的点的距离一定最小
     2. 与 t 不连通的点一定与起点不连通,$g[t][j]=dist[j]=\infty<dist[t]+g[t][j]=dist[t]+\infty$
    

    所以直接遍历所有点即可

每一次循环确定一个点的最短距离,循环 n - 1 次是因为最后一个点只更新了距离并且已经确定是最短距离,所以不需要再考虑加入st[n]的操作

重边保留长度最短的那条,自环不影响(在更新距离时,如果遍历到自己,dist[t] < dist[t] + g[t][t],所以dist[t]不变)

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定
memset(g, 0x3f, sizeof(g)); // 每条边初始化为正无穷
// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
        {
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        }
        st[t] = true; // 把这个点标记,收录到最短路径集合中
        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

2. 堆优化版dijkstra (稀疏图)

时间复杂度 O(mlogn), n 表示点数,m 表示边数

步骤:(这里的距离指的都是到出发点的距离)

  1. 起点的距离置为0,其他点的距离置为\(\infty\)
  2. 用小根堆来维护距离最小的点,由于一个点会进堆入度次,则已经确定最短路的可能在堆顶,每次取堆顶后赋给 t 后要判断,直到取到未确定最短路的、距离最小的点,并把这个点标记,收录到最短路径集合中。
  3. 用 t 更新它的邻点的距离,若(节点 t 的距离 + 节点 t 到该节点的边长)<该节点的距离,就更新该节点的距离,并将该距离进堆。
typedef pair<int, int> PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边,w[]储存边长
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定
// 加边,带权
void add(int a, int b, int c)
{
	e[idx]=b, w[idx]=c, ne[idx]=h[a], h[a]=idx++;    
}
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号,因为pair优先比较first

    // 堆空时所有与起点连通的点的最短路已经被算出来了
    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

Bellman-Ford算法(有负权边)

时间复杂度 O(nm), n 表示点数,m 表示边数

注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。

三角不等式:\(dist[b] \leq dist[a] + w\)

松弛操作:\(dist[b] = dist[a] + w\)

循环k次后dist[]的意义:从起点经过不超过k条边到达该点的最短距离(如果发生串联则不是,所以需要一个backup[]数组备份上一次迭代的结果,松弛操作用backup[]更新)

如果图中存在负权回路,最短路不一定存在,若负环不在路径上,则不影响

步骤:

  1. 起点的距离置为0,其他点的距离置为\(\infty\)
  2. 循环k次(k为允许经过的最大边数),每次对所有边都进行松弛操作\(dist[b] = dist[a] + w\)
  3. 循环结束之后,dist中存的就是最短路

注意:与起点不连通的点也可能被更新,所以要用dist[i] > inf / 2判断,并且不存在要返回inf,不是-1

int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在回路。由于经过n+1条边的dist比经过n条边的dist短,说明该回路一定是负权回路。
    // 循环n次表示经过n条边(不判断负环可改为n-1,因为只有n-1条边)
    for (int i = 0; i < n; i ++ )
    {
        // 对每条边都进行松弛操作
        for (int j = 0; j < m; j ++ )
        {
            auto t = edge[j];
            dist[t.b] = min(dist[t.b], dist[t.a] + t.w);
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return 0x3f3f3f3f; 
    return dist[n];
}
// if(bellman_ford()==0x3f3f3f3f) 不存在最短路
// else 存在 

spfa 算法(队列优化的Bellman-Ford算法)(无负环)

时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n 表示点数,m 表示边数

因为松弛操作:\(dist[b] = dist[a] + w\)\(dist[b]\)要减小就要求\(dist[a]\)减小,所有可以用一个队列存储所有变小的的点(要判重),当队列空的时候,迭代也不会改变dist[]的值,退出。

步骤:

  1. 起点的距离置为0,其他点的距离置为\(\infty\)
  2. 起点入队,打上标记
  3. 当队列不空时,取队头,去除标记,并用队头松弛所有邻边,并将成功松弛的点入队,打上标记
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    queue<int> q; 
    q.push(1);
    st[1] = true;
    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return dist[n];
}
// if(spfa() == 0x3f3f3f3f) 不存在最短路
// else 存在 

spfa判断图中是否存在负环

时间复杂度是 O(nm), n 表示点数,m 表示边数

spfa判负环有两种做法,去掉st数组会更快

  1. bfs做法:(较慢)
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中,可以不用

// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
    // 不需要初始化dist数组,当dist初始值为0时,代码会在负环处打转(负环内元素一直入队出队,cnt[]单调递增直到>=n)
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

    queue<int> q;
    // 把所有点都放在初始点集中,防止有从1号点到不了的负环
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环,且为负环(路更长距离反而更短)
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}
  1. dfs做法:(较快,注意dist一定要初始化为0)

    只需将bfs中的队列换成栈即可。

floyd算法 (无负环)

时间复杂度是 O(\(n^3\)), n 表示点数

重边取最短的,自环忽略

d[N][N]; // 邻接矩阵
初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    // 一定要先循环k
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
// if(d[a][b] > INF / 2) 不存在最短路
// else 存在 

最小生成树算法

一般是无向图,要同时加a->b,b->a两条边


朴素版prim算法(稠密图)

时间复杂度是\(O(n^2)\),n 表示点数,m 表示边数

步骤:(距离:点到集合内点的所有连线中的最短长度)

  1. 将所有点的距离初始化为\(+\infty\)
  2. 每次找到最小生成树集合外的距离最近的点赋给 t ,并将它标记,收录到最小生成树集合中
  3. 用 t 更新其他点的距离。如果该点到 t 的距离比当前该点到集合的距离短,就将该点的到集合的距离更新为到 t 的距离

每一次循环都将一个点加入最小生成树中,循环 n 次即可

重边保留长度最短的那条,自环会有影响,要先将新的树边长度加入权重之和中,再更新距离,否则dist[t] = min(dist[t], g[t][t])中,自环可能会修改dist[t],导致树边长度不正确

int prim()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0; // 规定一号点是起点,将它的dist提前置成0,可以减少特判
    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                 t = j;
        if (dist[t] == INF) return INF; // 一号点是0,不会return
        st[t] = true;
        res += dist[t]; // 一号点的dist是0,放心加
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], g[t][j]);
    }
    return res;
}

Kruskal算法(稀疏图)

时间复杂度是\(O(mlogm)\),n 表示点数,m 表示边数

步骤:

  1. 将所有边按权重从小到大排序。
  2. 枚举每条边a-b,权重c,如果a, b不在一个连通块(并查集维护),将这条边加入最小生成树集合中,并将a,b这两个连通块合并

重边、自环不影响

理解:将所有的边升序排列,从小到大遍历的过程中,每次都以最小的代价将两个点连通,如果这两个点已经连通,说明在之前已经用更小的代价将它们连通了。像这样,每个点的连通都是最优解,如果存在最小生成树,一定可以找到n - 1条边,保证每条边都是最优解,那么最终的权重之和就是最优解。如果找不到,说明不存在最小生成树。

int n, m;       // n是点数,m是边数
int p[N];       // 并查集的父节点数组,用并查集来维护连通块

struct Edge     // 存储边
{
    int a, b, w;

    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[M]; // M不小于总边数

int find(int x)     // 并查集核心操作
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集

    int res = 0, cnt = 0; // res是最小生成树的树边权重之和,cnt是当前最小生成树集合中的边数
    for (int i = 0; i < m; i ++ )
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b)     // 如果两个连通块不连通,则将这两个连通块合并,并将这条边加入最小生成树集合中
        {
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }

    if (cnt < n - 1) return INF; // 如果边数小于n-1条,说明图不连通,不存在最小生成树
    return res;
}

染色法判别二分图

时间复杂度是\(O(n+m)\),n 表示点数,m 表示边数

步骤:

  1. 将所有点置为未染色状态
  2. 遍历每个点,如果该点未染色,就以它为起点染色,如果返回了false,则不是二分图
  3. 染色:先将当前点染色,再遍历子节点。如果子节点未染色,则递归以它为起点染色,并检查返回值;如果子节点已经染色了,则判断是否同色,如果同色则不是二分图。
int n;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色

// 参数:u表示当前节点,c表示当前点的颜色
// 是二分图返回true, 不是返回false
bool dfs(int u, int c)
{
    color[u] = c;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (color[j] == -1)
        {
            if (!dfs(j, !c)) return false;
        }
        else if (color[j] == c) return false;
    }

    return true;
}
bool check()
{
    memset(color, -1, sizeof color);
    bool flag = true;
    // 尝试把所有点作为染色的起点,防止图不连通
    for (int i = 1; i <= n; i ++ )
        if (color[i] == -1)
            if (!dfs(i, 0))
            {
                flag = false;
                break;
            }
    return flag;
}

匈牙利算法

时间复杂度是\(O(nm)\),n 表示点数,m 表示边数,实际运行时间一般远小于\(O(nm)\)

二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。(即没有两条边共用一个点)

二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

步骤:

​ 对于每一个find,遍历该点的子节点,如果该子节点没被标记,就标记它。如果该子节点已经有匹配的节点了,就递归调用find,看能否为该子节点的匹配对象找到新的匹配,如果可以,就匹配该子节点,返回成功。如果所有的子节点都无法匹配,就返回失败。

int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];       // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];     // 表示第二个集合中的每个点是否已经被遍历过
bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            if (match[j] == 0 || find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }

    return false;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
    memset(st, false, sizeof st);
    if (find(i)) res ++ ;
}

树的直径

#include <bits/stdc++.h>
using namespace std;

using ll = long long;
using pii = pair<int, int>;
const int N = 1e5 + 10;

vector<pii> g[N];
int dist[N];

void dfs(int u, int fa, int distance) // 从u出发,u的父节点是fa,u到起点的距离是distance
{
    dist[u] = distance;
    for (auto [v, w]: g[u])
    {
        if (v == fa) continue;
        dfs(v, u, distance + w);
    }
}

int main()
{
    ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);

    int n;
    cin >> n;
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b, w;
        cin >> a >> b >> w;
        g[a].emplace_back(b, w), g[b].emplace_back(a, w);
    }
    dfs(1, -1, 0);
    int far = max_element(dist + 1, dist + n + 1) - dist;
    dfs(far, -1, 0);
    int diameter = *max_element(dist + 1, dist + n + 1);
    cout << 10 * diameter + (ll)diameter * (diameter + 1) / 2;
    return 0;
}

常用代码模板4——数学知识

快速幂

求a^k mod p,时间复杂度 O(logk)。

数据范围 1 <= a , p <= 2e9,0 <= k <= 2e9

注意:p如果很大,相乘的结果会在%p之前爆int,所以要开long long

// res 存结果
// t 存a^2^i, 每次都要平方取模, 初始值为a^2^0=a
// 如果k=0, 不会进入while循环, 所以要先把res初始化为1%p,这样, 如果k=0 && p>1, 那结果是1, 正确; 如果k=0 && p=1, 结果是0, 正确; 如果k!=0 && p=1, 则res=0, 循环后结果还是0, 正确; 如果k!=0 && p>1, 结果不影响, 正确
int qmi(int a, int k, int p)
{
    int res = 1 % p, t = a;
    while (k)
    {
        if (k & 1) res = res * t % p; // 这里可能爆int
        t = t * t % p; // 这里可能爆int
        k >>= 1;
    }
    return res;
}
模逆元

模逆元也称为模倒数,或者模逆元

b 存在模逆元\(b^{-1}\)的充要条件是 b 与模数 n 互质。

\((ab)^{-1}=a^{-1}\times b^{-1}\)

当模数 n 为质数时,\(b^{n-2}\)即为 b 的模逆元,用快速幂求解

// 求b模p的乘法逆元
if (b % p == 0) 无解
else 解 = qmi(b, p - 2, p);

常用代码模板5——动态规划

背包问题

01背包

模型:

有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

特点:每件物品仅有一件,可以选择放或不放。

// 边界:f[0][0~v] = 0
// c[]存物体的体积, w[]存物体的价值
// 二维数组
   for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= v; j ++ )
        {
            f[i][j]  = f[i - 1][j];
            if (j >= c[i]) 
                f[i][j] = max(f[i][j], f[i - 1][j - c[i]] + w[i]);
        }
    cout << f[n][v]; // 答案

// 一维数组
// 第i轮循环求的是从前i个物品中挑,总体积不大于0~v的选法的最大价值,结果存在f[]中。所以在第i轮循环开始时,f[]中存的是从前i-1个物品中挑选,总体积不大于0~v的选法的最大价值。由于总体积在0~c[i]-1的选法的最大价值不变,所以只需要更新总体积c[i]~v的选法的最大价值即可。又因为这一轮f[j]的更新要用到上一轮f[j - c[i]]的值,所以j要从最大值v开始递减到c[i], 保证在更新f[j]时f[j - v[i]]还是上一轮的结果。
    for (int i = 1; i <= n; i ++ )
        for (int j = v; j >= c[i]; j -- )
            f[j] = max(f[j], f[j - c[i]] + w[i]);

    cout << f[v];

完全背包

模型:

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

特点:每件物品有无限个

// 朴素做法
 for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
            for (int k = 0; k * v[i] <= j; k ++ )
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
// 二维数组
for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) 
                f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }
    cout << f[n][m];

// 一维数组
// 与01背包不同的是,对于总体积不大于v[i]~m的选法的最大值是用这一轮的f[j - v[i]]更新的,所以要从小到大循环,先算出小的。
for (int i = 1; i <= n; i ++ )
         for (int j = v[i]; j <= m; j ++ )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m];

多重背包

模型:

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件体积是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

特点:每件物品有限个,第i种物品有n[i]个

朴素做法类似于完全背包,枚举第i个物品选0~s[i]个,状态转移方程:

f[i][j] = max(f[i - 1][j - k * v[i]] + k * w[i]), k = 0, 1,.., s[i]

// 朴素做法
for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
            for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    cout << f[n][m];

二进制优化:将每个s[i] 分解成1, 2, 4, 8, ..., 2 ^ k, c,其中2 ^ (k + 1) < s[i], c = s[i] - (2^(k + 1) - 1),保证1~s[i]中的任何一个数可以由这些数组合出来,且这些数的总和刚好是s[i]。这样的话,我们就可以把s[i]件物品分别打包成1件, 2件, ..., c件,每一包就看成一个体积为k * v[i], 价值为k * w[i]的物品包,每个物品包只能选一次,转化成了01背包问题。将所有的物品包用01背包的方法来选,得到的结果与朴素做法相同。

int w[N], v[N], f[M];
int cnt, n, m;
    while (n -- )
    {
        int a, b, s;
        scanf("%d%d%d", &a, &b, &s);
        int k = 1;
        while (k <= s)
        {
            cnt ++ ;
            v[cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if (s) cnt ++ , v[cnt] = s * a, w[cnt] = s * b;
    }
    
    for (int i = 1; i <= cnt; i ++ )
        for (int j = m;  j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);
            
    cout << f[m];

分组背包

模型:

有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

特点:物品分组,每一组里最多选一件

// 二维
int n, m, v[N][N], w[N][N], s[N];
int f[N][N];

    for (int i = 1; i <= n; i ++ )
        for (int j = 0; j <= m; j ++ )
        {
            f[i][j] = f[i - 1][j];
            for (int k = 1; k <= s[i]; k ++ )
                if (v[i][k] <= j) f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
        }

    cout << f[n][m];
// 一维
for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= 0; j -- )
            for (int k = 1; k <= s[i]; k ++ )
                if (v[i][k] <= j) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

    cout << f[m];

数字三角形

初始化int为负无穷的方法:

0xcf :-8e8 (推荐)

0x8f:-1.8e9

0x9f:-1.6e9

// 自顶向下
    memset(f, 0xcf, sizeof f); // 初始化int为负无穷,每个是-8e8
 
    f[1][1] = a[1][1]; // 边界
    for (int i = 2; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = max(f[i - 1][j - 1], f[i - 1][j]) + a[i][j];
    
    int ans = -inf; // 目标
    for (int i = 1; i <= n; i ++ ) ans = max(ans, f[n][i]);
    cout << ans;

// 自下而上,从正下方和右下方
    for (int i = n; i >= 1; i -- )
        for (int j = i; j >= 1; j -- )
            f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + f[i][j];
            
    cout << f[1][1] << endl;

最长上升子序列(LIS)

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

分析:

  1. 朴素版本:\(O(n^2)\)

边界:f[i]初始值为1

目标:max{f[i]}, 1 <= i <= n

    for (int i = 1; i <= n; i ++ )
    {
        f[i] = 1;
        for (int j = 1; j <= i - 1; j ++ )
            if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
    }
    int ans = 0;
    for (int i = 1; i <= n; i ++ ) ans = max(ans, f[i]);
    cout << ans;

法二:二分优化:\(O(nlogn)\)

lower_bound(a, a + n, c),返回第一个大于等于c的数的地址。注意,数组存储范围是0~n-1,上界a+n是最后一位下一个的地址。如果c比数组中所有数都小,返回a, 如果c比数组中所有数都大,返回a+n

思路:

q[]中记录长度是i的子序列的最小结尾下标,对于a[i],在q[]找到最大的比a[i]小的数,a[i]的子序列最大长度就是那个数的子序列的长度+1。

关键:

  1. q[]数组一定升序。因为假设i = j + 1且长度为i的子序列的结尾小于长度为j的结尾,那么以长度为i的
    倒数第二个字符结尾的子序列长度为j且结尾一定比原长度为j的子序列的结尾要小,与原结尾是最小结
    尾矛盾。
  2. 长度相同的子序列,更小的结尾更好。更小的结尾更有可能被后面的a[i]接上。如结尾分别是7、5的长度
    相同的子序列。如果a[i]=6,那它可以接到5后面,a[i]的长度就可以更长。
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int a[N], q[N], len;

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


    for (int i = 1; i <= n; i ++ )
    {
        int l = lower_bound(q + 1, q + len + 1, a[i]) - q;
        len = max(len, l);
        q[l] = a[i];
    }

    cout << len;
    return 0;

}

最长公共子序列(LCS)

题目:给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

分析:

目标:f[N, M] ;边界:f[0, 0] = 0

// 下标从1开始 
    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]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }
    
    cout << f[n][m];

区间DP

所有的区间dp问题枚举时,第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)

for (int len = 1; len <= n; len++) {         // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1;                 // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

石子合并

目标:f[1][n]

状态计算:要合并[i, j]内的石堆,选定第k堆石子,先合并[i, k]和[k + 1, j],最后再将这两堆合并,这种方式可以枚举合并[i, j]石堆的所有方法。状态转移方程:

f[i][j] = min(f[i][k] + f[k + 1][j] + s[j] - s[i - 1]), i<= k<= j-1

在区间DP中,一个状态由若干个比它更小且包含于它的区间所代表的状态转移而来。所以区间的状态值应从小到大计算。

边界:f[i][i]= 0

for (int len = 2; len <= n; len ++ ) // len = 1时为0,不用算,区间长度从小到大
        for (int i = 1; i + len - 1 <= n; i ++ ) // 枚举长度为len的区间的左端点
        {
            int l = i, r = len + i - 1; // 每个状态的左右端点
            f[l][r] = 0x3f3f3f3f;
            for (int k = l; k <= r - 1; k ++ )
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
        
    cout << f[1][n] << endl;

常用代码模板6——贪心

区间选点

题目描述:

给定 N 个闭区间 [ai, bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

Snipaste_2023-01-25_17-41-48.png
区间选点.png

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

struct Range
{
    int l, r;
    bool operator< (const Range & W) const
    {
        return r < W.r;
    }
}ranges[N];

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &ranges[i].l, &ranges[i].r);
    
    sort(ranges, ranges + n);
    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
    {
        if (ranges[i].l > ed)
        {
            res ++ ;
            ed = ranges[i].r;
        }
    }
    cout << res;
    return 0;
}

最大不相交区间数量

题目描述:

给定 N 个闭区间 [ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。

输出可选取区间的最大数量。

思路

  1. 将所有区间按照右端点从小到大排序。
  2. 从第一个区间开始扫描所有区间,出现一个不相交区间当且仅当该区间的左端点大于当前维护的区间的右端点。

代码同上一题

区间分组

题目描述

给定 N 个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。

输出最小组数。

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;
priority_queue<int, vector<int>, greater<int>> q;
vector<PII> segs;

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        segs.push_back({a, b});
    }

    sort(segs.begin(), segs.end());

    q.push(2e9); // 让第一个区间可以加进来
    for (auto seg : segs)
    {
        if (q.top() >= seg.first) q.push(seg.second);
        else q.pop(), q.push(seg.second);
    }

    cout << q.size() - 1; // 多了一个
    return 0;
}

区间覆盖

题目描述

给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。

输出最少区间数,如果无法完全覆盖则输出 −1。

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;

struct Range
{
    int l, r;
    bool operator < (const Range & W) const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    int st, ed;
    scanf("%d%d", &st, &ed);

    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);

    int res = 0;
    bool flag = false;
    for (int i = 0; i < n; i ++ )
    {
        int j = i, r = -2e9;
        while (j < n && range[j].l <= st)
        {
            r = max(r, range[j].r);
            j ++ ;
        }

        if (r < st) break;

        res ++ ;
        if (r >= ed) 
        {
            flag = true;
            break;
        }
        else st = r, i = j - 1;
    }
    if (!flag) cout << -1;
    else cout << res;
    return 0;
}

排序不等式

题目描述

有 n 个人排队到 11 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int w[N];
int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++ ) scanf("%d", &w[i]);
    sort(w, w + n);
    LL ans = 0;
    for (int i = 0, j = n - 1; i < n; i ++ , j -- )
        ans += w[i] * j;
    printf("%lld", ans);
    return 0;
}

绝对值不等式

题目描述

在一条数轴上有 N 家商店,它们的坐标分别为 A1∼AN。

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

做法

货仓的坐标选在所有数的中位数,如果是偶数的话可以选择中间两个中的任意一个

耍杂技的牛

题目描述

农民约翰的 N 头奶牛(编号为 1..N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

这 N 头奶牛中的每一头都有着自己的重量 Wi 以及自己的强壮程度 Si。

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;
const int N = 50010;
PII a[N];

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++ ) 
    {
        int w, s;
        scanf("%d%d", &w, &s);
        a[i] = {w + s, w};
    }
    sort(a, a + n);
    int ans = -2e9, sum = 0;
    for (int i = 0; i < n; i ++ )
    {
        int w = a[i].second, s = a[i].first - a[i].second;
        ans = max(ans, sum - s);
        sum += w;
    }
    cout << ans;
    return 0;
}

题目

整数转化为string

to_string()

旅行环游问题

#include <bits/stdc++.h>
using namespace std;

#define dbg(...) fprintf(stderr, __VA_ARGS__)
using ll = long long;
using pii = pair<int, int>;
const int N = 2e6 + 10;
int q[N], hh, tt = -1;
int oil[N], m[N];
ll s[N];
bool st[N];
int main()
{
    ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
    int n;
    cin >> n;
    for (int i = 1; i <= n; i ++ ) cin >> oil[i] >> m[i];
    for (int i = 1; i <= n; i ++ ) s[i] = s[i + n] = oil[i] - m[i];
    for (int i = 1; i <= 2 * n - 1; i ++ ) s[i] += s[i - 1];
    hh = 0, tt = -1;
    for (int i = 1; i <= 2 * n - 1; i ++ )
    {
        while (hh <= tt && (i - q[hh] + 1) > n) hh ++ ;
        while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
        q[ ++ tt] = i;
        if (i >= n)
        {
            if (s[q[hh]] - s[i - n] >= 0) st[i - n + 1] = true;
        }
    }
    m[0] = m[n];
    for (int i = 1; i <= n; i ++ ) s[i] = s[i + n] = oil[n + 1 - i] - m[n - i];
    for (int i = 1; i <= 2 * n - 1; i ++ ) s[i] += s[i - 1];

    hh = 0, tt = -1;
    for (int i = 1; i <= 2 * n - 1; i ++ )
    {
        while (hh <= tt && (i - q[hh] + 1) > n) hh ++ ;
        while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
        q[ ++ tt] = i;
        if (i >= n)
        {
            if (s[q[hh]] - s[i - n] >= 0) st[2 * n - i] = true;
        }
    }
    for (int i = 1; i <= n; i ++ )
        cout << (st[i] == true ? "TAK\n" : "NIE\n");
    return 0;
}

导弹防御系统

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int N = 60;
int h[N], up[N], down[N], cntup, cntdown, n, ans = 1e9;
void dfs(int u)
{
    if (cntup + cntdown >= ans) return;
    if (u == n)
    {
        ans = min(ans, cntdown + cntup);
        return;
    }
    // 上升的,接在最大的比h[u]小的数后面
    bool up_flag = true;
    for (int i = 0; i < cntup; i ++ )
    {
        if (up[i] < h[u])
        {
            up_flag = false;
            int t = up[i];
            up[i] = h[u];
            dfs(u + 1);
            up[i] = t;
            break;
        }
    }
    // 如果没有,自己开一个上升的,也就是说,新开的数一定比原来的所有的上升序列的末尾数小
    // 所以up数组是单调递减的
    if (up_flag)
    {
        up[cntup ++ ] = h[u];
        dfs(u + 1);
        cntup -- ;
    }
    // 下降的,接在最小的比h[u]大的数后面
    bool down_flag = true;
    for (int i = 0; i < cntdown; i ++ )
    {
        if (down[i] > h[u])
        {
            down_flag = false;
            int t = down[i];
            down[i] = h[u];
            dfs(u + 1);
            down[i] = t;
            break;
        }
    }
    // 如果没有,自己开一个下降的,也就是说,新开的数一定比原来的所有的下降序列的末尾数大
    // 所以down数组是单调递增的
    if (down_flag)
    {
        down[cntdown ++ ] = h[u];
        dfs(u + 1);
        cntdown -- ;
    }
}
void solve()
{
    memset(up, 0, sizeof up), memset(down, 0, sizeof down), cntup = 0, cntdown = 0, ans = 1e9; // 每次都初始化
    for (int i = 0; i < n; i ++ ) cin >> h[i];
    dfs(0);
    cout << ans << "\n";
}
int main()
{
    ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
    while (cin >> n, n)
        solve();
    return 0;
}

生日蛋糕

#include<iostream>
#include<cmath>
using namespace std;
const int N = 24, INF = 1e9;
int n, m;
int minv[N], mins[N];
int res = INF;
//记录每层的半径和高,因为会用到上一层的高度
int R[N], H[N];
//u当前层次,v当前处理的体积和,s当前处理的面积和
void dfs(int u, int v, int s)
{
    if(v + minv[u] > n) return;
    if(s + mins[u] >= res) return;
    if (s + 2 * (n - v) / R[u + 1] >= res) return;
    if(!u)
    {
        if(v == n) res = s;
        return;
    }
    //搜索顺序,先R后H,从大到小
    for(int r = min(R[u + 1] - 1,(int)sqrt((n - v - minv[u - 1]) / u)); r >= u; r--)
        for(int h = min(H[u + 1] - 1, (n - v - minv[u - 1]) / r / r); h >= u; h--)
        {
            H[u] = h, R[u] = r;
            //最底层的时候需要加上r*r
            int t = u == m ? r * r : 0;

            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
}
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
    {
        minv[i] = minv[i - 1] + i * i * i;
        mins[i] = mins[i - 1] + 2 * i * i;
    }
    //m+1层不存在,作为哨兵,减少边界情况的判断
    R[m + 1] = H[m + 1] = INF;
    dfs(m, 0, 0);
    if(res == INF) res = 0;
    cout << res << endl;
    return 0;
}
posted @ 2023-07-07 08:42  sakuraLGGM  阅读(10)  评论(0编辑  收藏  举报