算法刷题总结

1.前缀和算法

eg1:P8160 [JOI 2022 Final] 星际蛋糕

思想:暴力枚举------>前缀和+二分优化 

#include <algorithm>
#include <iostream>
using namespace std;
int ans;
const int N = 200005;
long long n, q,a[N], num[N], s[N], x;//不开long long见祖宗!!!!
int main()
{
    scanf("%lld", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", &a[i]);
        num[i] = 1;
        while (!(a[i] & 1))//a[i] & 1 的含义是判断 a[i] 是否为奇数,因为 1 的二进制表示是 
                           //0001,而奇数的二进制表示的最低位一定是 1。
        {
            a[i] >>= 1;
            num[i] <<= 1;
        }
        s[i] = s[i - 1] + num[i];//求数量前缀和
    }
    scanf("%lld", &q);
    while (q--)
    {
        scanf("%lld", &x);
        int ans = a[(lower_bound(s + 1, s + 1 + n, x) - s)];//lower_bound,C++库里内置的二分函数,返回区间中第一个大于等于 x 的数的地址,减去s就可以得到下标。
        printf("%d\n", ans);
    }
}

tips:

  1. 前缀和,差分,循环下标都从1开始

  2. 整体被切分成若干个体,并计算某个区间的和----->前缀和思想

  3. C++STL二分查找lower_bound 

 2.双指针算法

eg1:P7714 「EZEC-10」排列排序

 思想:双指针维护单个序列(快慢指针)

小技巧:因为此题为从1---n的升序序列,所以数组的下标对应该元素的值。(数字1的下标为1,数字2的下标为2,依此类推)

#include <algorithm>
#include <iostream>
using namespace std;
int n, T, a[1000005], maxx, ans;
int main()
{
    scanf("%d", &T);
    while (T--)
    {
        ans = 0;//每一次操作前进行初始化
        scanf("%d", &n);
        for (int i = 1; i <= n; i++)
            scanf("%d", &a[i]);
        int i = 1;
        while (i <= n)
        {
            if (a[i] == i)//数组的下标与该元素的值相同,i指针向后移动一位
                i++;
            else
            {
                int maxx = a[i];//maxx为j指针的移动的最大范围
                int j = i + 1;
                maxx = max(maxx, a[j]);//j指针移动的最远位置不能超过i--j之间的元素最大值
                while (maxx > j)
                {
                    j++;
                    maxx = max(maxx, a[j]);
                }
                ans += j - i + 1;
                i = j + 1;
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

3.区间合并

eg1:P1496 火烧赤壁

思想1:区间合并

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef pair<long long, long long> pll;//题目数据范围较大,开long long
int n;
vector<pll> segs;
long long ans;
void merge(vector<pll> &segs)
{
    vector<pll> res;
    sort(segs.begin(), segs.end());//将区间的起点排序
    long long st = -2e31, ed = -2e31;//初始化:起点和终点都为数据范围最小值-2e31
    for (auto seg : segs)
        if (ed < seg.first)//若前一段区间的终点小于下一段区间的起点,则增加一个新的区间
        {
            if (st != -2e31)
                res.push_back({st, ed});
            st = seg.first, ed = seg.second;
        }
        else
            ed = max(ed, seg.second);//否则更新区间的终点
    if (st != -2e31)
        res.push_back({st, ed});//防止输入的空间segs为空
    segs = res;
}
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
    {
        long long l, r;
        scanf("%lld%lld", &l, &r);
        segs.push_back({l, r});
    }
    merge(segs);
    for (auto seg : segs)
        ans += seg.second - seg.first;
    printf("%lld", ans);
    return 0;
}

思想2:离散化+差分

思路 :

  1. 题目数据范围大,个数少----->离散化
  2. 问题转化为在一个数列上,每次给 [l,r] 区间内的数都增加 1。 最后求出数列的末状态,看有多少个点不是 0。但是我们直接模拟的话时间复杂度会过高,所以我们可以在离散化后进行差分,最后进行求前缀和。
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef pair<int, int> pii;
int n, ans = 0;
const int N = 4e4 + 5;//每组数据有两个,所以开两倍才能存完
int b[N], s[N];
vector<int> alls;//存储所有待离散化的值
vector<pii> add, query;
int find(int x)//二分求出x对应离散化的值
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = (l + r) / 2;
        if (alls[mid] >= x)
            r = mid;
        else
            l = mid + 1;
    }
    return r + 1;//+1时方便做差分求和(前缀和从1开始比较方便,防止下标是-1的越界)
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        int l, r;
        scanf("%d%d", &l, &r);
        query.push_back({ l, r });
        alls.push_back(l);
        alls.push_back(r);
    }
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls.begin(), alls.end()), alls.end());
    for (auto item : query)
    {
        int l = find(item.first), r = find(item.second);
        b[l]++, b[r]--;//这里+1和二分+1二选一
    }                  //题目说明不包含右端点,因此为b[r]--,不是b[r+1]--
    for (int i = 1; i <= alls.size(); i++)
        s[i] += s[i - 1] + b[i];//差分并求前缀和
    int l, r;//l表示起始位置下标,r表示终止位置下标
    //以样例为例
    //  -1 1 2 5 9 11   alls[]
    //   1 2 3 4 5 6    离散后的数组下标
    // 0 1 0 1 2 1 0    s[]  
    //第一段火是[-1,1),s[i]>0,s[i-1]==0为起始位置,s[i]==0,s[i-1]>0为终止位置

    for (int i = 1; i <= alls.size(); i++)
    {
        if (s[i] != 0 && s[i - 1] == 0)
            l = i - 1;//l是差分数组的下标,实际下标要-1
        //if (s[i] != 0 && s[i + 1] == 0)
        //{
        //    r = i;//i+1的数才是终点,再-1得i
        //    ans += alls[r] - alls[l];//通过下标取出alls数组中实际的值
        //}

        if (s[i] == 0 && s[i - 1] != 0)
        {
            r = i - 1;//i是终点,再-1得i-1
            ans += alls[r] - alls[l];//通过下标取出alls数组(离散化前的数组)中实际的值
        }//两个if一个意思
    }
    printf("%d", ans);
    return 0;
}

4.链表

eg1:4654. 消除游戏 

 思想:双链表快速完成删除操作

#include <algorithm>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e6 + 10;
char s[N];
vector<int> w, q;//w为待删数组,q为备份数组
int l[N], r[N];
bool st[N];
void insert(int k)
{
    if (!st[k])//特判:防止某个数据被多次删除  eg:$$@@
    {
        st[k] = true;
        w.push_back(k);
    }
}
void fun()
{
    w.clear();
    for (int k : q)
    {
        int a = l[k], b = k, c = r[k];
        if (s[a] == s[b] && s[b] != s[c] && s[c] != '#')
        {
            insert(b);
            insert(c);
        }
        else if (s[a] != s[b] && s[b] == s[c] && s[a] != '#')
        {
            insert(a);
            insert(b);
        }
    }
}
int main()
{
    scanf("%s", s + 1);
    int n = strlen(s + 1);
    s[0] = s[n + 1] = '#';//规定链表两端边界,防止越界问题
    for (int i = 1; i <= n; i++)
    {
        l[i] = i - 1, r[i] = i + 1;
        q.push_back(i);
    }//构建链表数据结构
    r[0] = 1, l[n + 1] = n;//初始化链表
    while (true)
    {
        fun();
        if (w.empty())//判空
            break;
        q.clear();
        for (int k : w)
        {
            int a = l[k], b = k, c = r[k];
            if (!st[a] && a && (q.empty() || a != q.back()))
                q.push_back(a);
            if (!st[c] && c != n + 1)
                q.push_back(c);
            r[a] = c, l[c] = a;//双链表的删除操作
        }
    }
    if (r[0] == n + 1)
        puts("EMPTY");
    else
    {
        for (int i = r[0]; i != n + 1; i = r[i])
            printf("%c", s[i]);
    }
    return 0;
}

eg2:136. 邻值查找 

思想:双链表快速完成删除操作

思路:

  1. 寻找与A[i]差值最小的数------>排序操作(排序后A[i]左右两个数即为差值最小的数)
  2. 先从第n个数开始枚举,然后逆序输出----->因为题目中说寻找A[i]之前与A[i]差值最小的数,所有数都在n前面,不用担心顺序问题
  3. 第n个数枚举完后,删除第n个数,继续向前遍历
#include <algorithm>
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
typedef pair<long long, int> pli;
int n;
int p[N], l[N], r[N];//p[n]存储排序后该数在链表中所对应的下标
pli a[N], ans[N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i].first;
        a[i].second = i;
    }
    sort(a + 1, a + 1 + n);
    for (int i = 1; i <= n; i++)
    {
        l[i] = i - 1, r[i] = i + 1;//初始化链表
        p[a[i].second] = i;
    }
    a[0].first = -4e9, a[n + 1].first = 4e9;//设置哨兵防止越界
    for (int i = n; i > 1; i--)
    {
        int j = p[i], left = l[j], right = r[j];
        long long lv = abs(a[left].first - a[j].first);
        long long rv = abs(a[right].first - a[j].first);
        if (lv <= rv)
            ans[i] = {lv, a[left].second};
        else
            ans[i] = {rv, a[right].second};
        l[right] = left, r[left] = right;//删除第n个节点
    }
    for (int i = 2; i <= n; i++)
        cout << ans[i].first << ' ' << ans[i].second << endl;
    return 0;
}

5.栈

eg1:150. 括号画家 

思想:利用栈完成括号匹配

小技巧:括号序列常利用栈的数据结构

#include <algorithm>
#include <cstring>
#include <iostream>
#include <stack>
using namespace std;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int res = 0;
    string str;
    cin >> str;
    stack<int> stk;//利用栈存储括号序列的下标,方便计算美观括号序列的长度
    for (int i = 0; i < str.size(); i++)
    {
        char c = str[i];
        if (stk.size())
        {
            char t = str[stk.top()];
            if (c == ')' && t == '(' || c == ']' && t == '[' || c == '}' && t == '{')
                stk.pop();
            else
                stk.push(i);
        }
        else
            stk.push(i);
        if (stk.size())
            res = max(res, i - stk.top());
        else
            res = max(res, i + 1);
    }
    cout << res << endl;
    return 0;
}

eg2:1883. 删减 (字符串中删除指定子串)

思想:栈----->只用在结尾进行增删操作

#include <algorithm>
#include <cstring>
#include <iostream>
#include <stack>
using namespace std;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    string s, t;
    cin >> s >> t;
    string stk;//利用STL中的string模拟栈
    for(auto c:s)
    {
        stk += c;
        while(stk.size()>=t.size()&&stk.substr(stk.size()-t.size())==t)
            stk.erase(stk.end() - t.size(), stk.end());
    }
    cout << stk << endl;
    return 0;
}

eg3:131. 直方图中最大的矩形

思想:单调栈------>找出每个数左(右)边离它最近的比它大(小)的数

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;
int n;
int h[N], l[N], r[N], stk[N];//栈存的是数组的下标
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    while (scanf("%d", &n), n)//输入包含几个测试用例,当输入用例为n=0时,结束输入
    {
        for (int i = 1; i <= n; i++)
            scanf("%d", &h[i]);
        h[0] = h[n + 1] = -1;//设置哨兵防止数组越界
        int tt = 0;
        stk[0] = 0;
        for (int i = 1; i <= n; i++)//找出每个数左边离它最近的比它小的数
        {
            while (tt && h[i] <= h[stk[tt]])
                tt--;//删除栈顶
            l[i] = stk[tt];
            stk[++tt] = i;
        }
        tt = 0;//将栈更新成只有一个元素
        stk[0] = n + 1;
        for (int i = n; i > 0; i--)//找出每个数右边离它最近的比它小的数
        {
            while (tt && h[i] <= h[stk[tt]])
                tt--;//删除栈顶
            r[i] = stk[tt];
            stk[++tt] = i;
        }
        ll res = 0;
        for (int i = 1; i <= n; i++)
            res = max(res, (ll)h[i] * (r[i] - l[i] - 1));
        printf("%lld\n", res);
    }
    return 0;
}

eg4:152. 城市游戏

思想:模拟/递推+单调栈

思路:本题为eg3的加强版,可将模型抽象成eg3(见下图)

#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int n, m;
int s[N][N], l[N], r[N], stk[N];
int work(int h[])//h表示每个矩形高度(这个函数就是eg3的原代码)
{
    h[0] = h[m + 1] = -1;
    int tt = 0;
    stk[0] = 0;
    for (int i = 1; i <= m; i++)
    {
        while (tt && h[i] <= h[stk[tt]])
            tt--;
        l[i] = stk[tt];
        stk[++tt] = i;
    }
    tt = 0;
    stk[0] = m + 1;
    for (int i = m; i > 0; i--)
    {
        while (tt && h[i] <= h[stk[tt]])
            tt--;
        r[i] = stk[tt];
        stk[++tt] = i;
    }
    int res = 0;
    for (int i = 1; i <= m; i++)
        res = max(res, h[i] * (r[i] - l[i] - 1));
    return res;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
        {
            char c;
            cin >> c;
            if (c == 'F')
                s[i][j] = s[i - 1][j] + 1;//递推求出累计F及其上方一共有几个F
        }
    int res = 0;
    for (int i = 1; i <= n; i++)
        res = max(res, work(s[i]));
    cout << 3 * res << endl;
    return 0;
}

6.队列

eg1:496. 机器翻译 

思想:队列------>先进先出

#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e3 + 10;
int n, m;
int res = 0;
bool st[N];//判断是否在队列中
queue<int> q;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> m >> n;
    while (n--)
    {
        int x;
        cin >> x;
        if (!st[x])
        {
            if (q.size() == m)
            {
                st[q.front()] = false;
                q.pop();
            }
            q.push(x);
            st[x] = true;
            res++;
        }
    }
    cout << res << endl;
    return 0;
}

eg2:154. 滑动窗口

思想:单调队列

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int a[N], q[N];//队列中存的是数组下标
int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i++)
        cin >> a[i];
    int hh = 0, tt = -1;//初始化队列
    for (int i = 0; i < n; i++)
    {
        while (hh <= tt && q[hh] < i - m + 1)//判断队头是否划出窗口
            hh++;//弹出队头
        while (hh <= tt && a[q[tt]] >= a[i])
            tt--;//弹出队尾
        q[++tt] = i;//向队尾插入一个数
        if (i >= m - 1)
            cout << a[q[hh]]<<" ";
    }
    puts("");
    hh = 0, tt = -1;
    for (int i = 0; i < n;i++)
    {
        while (hh <= tt && q[hh] < i - m + 1)//判断队头是否划出窗口
            hh++;//弹出队头
        while (hh <= tt && a[q[tt]] <= a[i])
            tt--;//弹出队尾
        q[++tt] = i;//向队尾插入一个数
        if (i >= m - 1)
            cout << a[q[hh]]<<" ";
    }
    return 0;
}

eg3:1162. 公交换乘 

思想:滑动窗口+队列模拟

思路:

我们可以从前往后扫描每条记录,同时用一个队列维护当前车次可以使用的优惠券区间(区间为45分钟):

  1.  如果当前记录是火车,则加入维护的优惠券区间;
  2. 如果当前记录是公交车,则线性扫描一遍队列中所有优惠券,找到第一个未被使用过的且大于等于当前价格的优惠券即可;
  3. 可以用一个bool数组对优惠券判重,以保证每张优惠券最多只被用一次。
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int res = 0;
struct ticket
{
    int time, price;
    bool used;
} q[N];
int main()
{
    scanf("%d", &n);
    int l = 0, r = 0;//队列初始化
    for (int i = 0; i < n; i++)
    {
        int type, price, time;
        scanf("%d%d%d", &type, &price, &time);
        if (type == 0)
        {
            res += price;
            q[r++] = {time, price};
        }
        else
        {
            while (l <= r && time - q[l].time > 45)//维护一个滑动窗口
                l++;
            bool success = false;
            for (int j = l; j <= r; j++)
            {
                if (q[j].used == false && q[j].price >= price)
                {
                    q[j].used = true;
                    success = true;
                    break;
                }
            }
            if (!success)
                res += price;
        }
    }
    printf("%d\n", res);
    return 0;
}

eg4:4964. 子矩阵

思想:二维滑动窗口

思路:二维的单调队列,等价于a个一维的情况中的最小值

第一步:对于每一行,求长度为b的窗口的最大值/最小值
第二步:在第一步的基础上,对于每一列,求长度(可以理解为高度)为a的窗口的最大值/最小值

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e3 + 10, mod = 998244353;
typedef long long ll;
int n, m,a,b;
int w[N][N], rmax[N][N], rmin[N][N], q[N];
int res = 0;
void get_max(int a[], int b[], int tot, int k)//滑动窗口求最大值
{
    int hh = 0, tt = -1;
    for (int i = 0; i < tot; i++)
    {
        while (hh <= tt && q[hh] <= i - k)
            hh++;
        while (hh <= tt && a[q[tt]] <= a[i])
            tt--;
        q[++tt] = i;
        b[i] = a[q[hh]];//取出队首元素
    }
}
void get_min(int a[], int b[], int tot, int k)//滑动窗口求最小值
{
    int hh = 0, tt = -1;
    for (int i = 0; i < tot; i++)
    {
        while (hh <= tt && q[hh] <= i - k)
            hh++;
        while (hh <= tt && a[q[tt]] >= a[i])
            tt--;
        q[++tt] = i;
        b[i] = a[q[hh]];//取出队首元素
    }
}
int main()
{
    cin >> n >> m >> a >> b;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            cin >> w[i][j];
    for (int i = 0; i <n; i++)//对于每一行,求长度为b的窗口的最大值/最小值
    {
        get_max(w[i], rmax[i], m, b);
        get_min(w[i], rmin[i], m, b);
    }
    int A[N], B[N], C[N];
    for (int i = b - 1; i < m; i++)//在第一步的基础上,对于每一列,求长度(可以理解为高度)为a 
                                   //的窗口的最大值/最小值
    {
        for (int j = 0; j < n; j++)
            A[j] = rmax[j][i];
        get_max(A, B, n, a);
        for (int j = 0; j < n; j++)
            A[j] = rmin[j][i];
        get_min(A, C, n, a);
        for (int j = a - 1; j < n; j++)
            res = (res + (ll)B[j] * C[j]) % mod;//转换成long long,防止爆int
    }
    cout << res << endl;
    return 0;
}

7.KMP

eg1:141. 周期 

 思想:求next数组------>最小循环节t=n-next[n]

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
int n;
char p[N];
int ne[N];//next数组:最大的后缀与前缀相等
int main()
{
    int T = 1;
    while (scanf("%d", &n), n)
    {
        scanf("%s", p + 1);
        for (int i = 2, j = 0; i <= n; i++)//KMP算法求next数组
        {
            while (j && p[i] != p[j + 1])
                j = ne[j];
            if (p[i] == p[j + 1])
                j++;
            ne[i] = j;
        }
        printf("Test case #%d\n", T++);
        for (int i = 1; i <= n; i++)
        {
            int t = i - ne[i];//求循环节t
            if (i % t == 0 && i / t > 1)
                printf("%d %d\n", i, i / t);
        }
        puts("");
    }
    return 0;
}

eg2:3823. 寻找字符串 

思想:用next数组求字符串中所有的前缀=后缀

思路:

  1. 如果说l1=next[i]是最长的公共前后缀,那么次长的公共前后缀是l2=next[l1],再次长的公共前后缀是l3=next[l2].........依此类推可以求字符串中所有的公共前后缀
  2. 把所有的next[i]都存到一个bool数组里面去,然后找到第一个在前面出现过的数,就是我们的答案

------>如果这个长度的字符串也被标记过,那么就说明满足要求,

          如果不合法的话,就再找当前最长公共前后缀的最长公共前后缀,那么这个最长公共前后缀就            会在前面出现过,然后也会在中间出现过

#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e6 + 10;
char p[N];
int ne[N];
bool st[N];
int main()
{
    int T;
    scanf("%d",&T);
    while (T--)
    {
        scanf("%s", p + 1);
        n = strlen(p + 1);
        for (int i = 2, j = 0; i <= n; i++)//求next[]数组
        {
            while (j && p[i] != p[j + 1])
                j = ne[j];
            if (p[i] == p[j + 1])
                j++;
            ne[i] = j;
        }
        for (int i = 0; i <= n; i++)//先初始化
            st[i] = false;
        for (int i = 1; i<n; i++)//标记的是2~n-1所有出现过的数
            st[ne[i]] = true;
        int res = 0;
        for (int i = ne[n]; i; i = ne[i])
            if (st[i])
            {
                res = i;
                break;
            }
        if(!res)
            puts("not exist");
        else 
        {
            p[res + 1] = 0;//输出技巧,直接打上结尾标记,后面的就直接不输出了
            printf("%s\n", p + 1);
        }
    }
    return 0;
}

8.Trie树(字典树、前缀树) 

一般来说,用到 Trie的题目中的字母要么全是小写字母,要么全是大写字母,要么全是数字,要么就是0和1,也就是说字符的个数不是很多。

------>主要处理前缀问题 

insert(插入) 

当需要插入一个字符串 s 时,我们令一个指针 p起初指向根节点。 

然后,依次扫描 s中的每个字符 c,当 s中的字符扫描完毕时,在当前指针 p上标记它是一个字符串的末尾。

eg1:142. 前缀统计

#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int son[N][26];//存储树中每个节点的子节点
int cnt[N];//存储以每个节点结尾的单词数量
char str[N];
int idx;//指向当前节点
void insert(char *str)//插入字符串
{
    int p = 0;//初始化根节点
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if (!son[p][u])
            son[p][u] = ++idx;//若无此节点,添加此节点
        p = son[p][u];//向前遍历
    }
    cnt[p]++;//标记当前节点存在单词
}
int query(char *str)//查询字符串出现的次数
{
    int p = 0,res=0;
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if (!son[p][u])
            return res;
        p = son[p][u];
        res += cnt[p];
    }
    return res;
}
int main()
{
    scanf("%d%d", &n, &m);
    while (n--)
    {
        scanf("%s", str);
        insert(str);
    }
    while (m--)
    {
        scanf("%s", str);
        printf("%d\n", query(str));
    }
    return 0;
}

eg2:161. 电话列表 

思路:

  1. 每次先把号码一个一个存进str数组,每个数字位都++
  2. 判断如果一个号码每一位出现次数均大于1,说明其为前缀,输出”NO” 
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;
int n, m;
int son[N][10];
int cnt[N];
char str[N][10];
int idx;
void insert(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - '0';
        if (!son[p][u])
            son[p][u] = ++idx;
        p = son[p][u];
        cnt[p]++;//把号码一个一个存进str数组,每个数字位都++
    }
}
bool check(char *str)
{
    int p = 0;
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - '0';
        if (cnt[son[p][u]] == 1)//如果一个号码为1,那么这个号码之后的所有号码次数都为1
            return true;
        p = son[p][u];
    }
    return false;
}
int main()
{
    scanf("%d", &n);
    while (n--)
    {
        int flag = 0;
        idx=0;//每次操作前进行初始化
        memset(son[0], 0, sizeof son);
        memset(cnt, 0, sizeof cnt);
        scanf("%d", &m);
        for (int i = 0; i < m; i++)
        {
            scanf("%s", str[i]);
            insert(str[i]);
        }
        for (int i = 0; i < m; i++)
        {
            if (!check(str[i]))
            {
                flag = 1;
                break;
            }
        }
        if (flag == 1)
            puts("NO");
        else
            puts("YES");
    }
    return 0;
}

eg3:4191. 加密信息 

思路:此题相当于eg1和eg2的综合版(N串存入Trie数中,M串为被查询串)

  1. 当M串比N串长度大 ,运用eg1的思路,可统计所有前缀数量
  2. 当M串比N串长度小,则运用eg2的思路
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 500010;
int n, m;
int son[N][2];
int cnt[N], st[N];
char str[N][10];
int idx;
void insert(string str)
{
    int p = 0;
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - '0';
        if (!son[p][u])
            son[p][u] = ++idx;
        p = son[p][u];
        st[p]++;//这里是记录经过p结点的字符串数量,因为这里可能会重复经过某一个结点就例如
                //样例中的第二条解密信息,他经过了3个相同的1,这里就是为了记录那种情况
    }
    cnt[p]++;//这里就是记录以p为结点的字符串的数量
}
int query(string str)
{
    int p = 0, res = 0;
    for (int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if (!son[p][u])//处理的是M串比N串长度大的情况,如果这个点接下来不再匹配
                       //返回的就是以这个点为尾结点的字符串数量
            return res;
        p = son[p][u];
        res += cnt[p];
    }
    return res + st[p] - cnt[p];//这里就是处理M串比N串中字符串长度小的情况
                                //st[p]-ed[p]就是p结点之后的字符串的个数
}
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        int k;
        string s;
        cin >> k;
        for (int j = 1; j <= k; j++)//因为空格也是字符,运用字符串拼接防止空格被读入字 
                                    //符串
        {
            char c;
            cin >> c;
            s += c;
        }
        insert(s);
    }
    for (int i = 1; i <= m; i++)
    {
        int k;
        string s;
        cin >> k;
        for (int j = 1; j <= k; j++)
        {
            char c;
            cin >> c;
            s += c;
        }
        printf("%d\n", query(s));
    }
    return 0;
}

9.并查集

eg1:3719. 畅通工程 

思路:用并查集来维护多个集合,若两个元素不在同一个集合当中,则将这两个元素添加到一个集合当中形成联通块,联通块的数量-1即为答案 

#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 10010;
int n, m;
int p[N];
int find(int x)
{
    if(p[x]!=x)
        p[x] = find(p[x]);
    return p[x];
}
int main()
{
    scanf("%d%d", &n,&m);
    for (int i = 1; i <= n;i++)
        p[i] = i;
    int cnt = n;//初始化:每个元素自己与自己连接(类似于n个联通块)
    for (int i = 1; i <= m;i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        if(find(a)!=find(b))
        {
            p[find(a)] = find(b);
            cnt--;
        }
    }
    printf("%d", cnt - 1);
    return 0;
}

10.堆(二叉树结构) 

eg1:4072. 习题册 

思想:小根堆

思路:

  1. 实现删除任意一个元素------->堆
  2. 学生每次挑选最便宜的练习册---------->小根堆 
#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int N = 2e5 + 10;
typedef pair<int, int> pii;
int n, m;
int p[N];
priority_queue<pii, vector<pii>, greater<pii>> h[3];//开三个小根堆存三种知识点的书
bool st[N];//判断该书是否已经买过
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%d", &p[i]);
    for (int j = 0; j < 2; j++)
        for (int i = 0; i < n; i++)
        {
            int a;
            scanf("%d", &a);
            h[a - 1].push({p[i], i});
        }
    scanf("%d", &m);
    while (m--)
    {
        int c;
        scanf("%d", &c);
        c--;
        while (h[c].size() && st[h[c].top().second])
            h[c].pop();
        if (h[c].empty())
            printf("-1 ");
        else
        {
            auto t = h[c].top();
            h[c].pop();
            printf("%d ", t.first);
            st[t.second] = true;
        }
    }
    return 0;
}

eg2:54. 数据流中的中位数

思想:处理动态中位数-------->对顶堆

思路:

  1. 大根堆:维护集合中较小值的部分的最大值。 
  2. 小根堆:维护集合中较大值的部分的最小值。

如果两个堆的大小相差不超过1,较大的那个堆的堆顶必定是中位数(偶数个数时中位数是排序后中间的两个之一)

class Solution
{
public:
    priority_queue<int, vector<int>, greater<int>> min_heap;
    //小根堆:维护集合中较大值的部分的最小值
    priority_queue<int> max_heap;//大根堆:维护集合中较小值的部分的最大值
    void insert(int num)
    {
        max_heap.push(num);//将所有插入的数都先加到大根堆,然后在进行交换
        if (min_heap.size() && max_heap.top() > min_heap.top())
        {//如果大根堆的堆头>小根堆的堆头,则交换两个数
            auto maxv = max_heap.top(), minv = min_heap.top();
            max_heap.pop(), min_heap.pop();
            max_heap.push(minv), min_heap.push(maxv);
        }
        if (max_heap.size() > min_heap.size() + 1)
        {//如果两个堆的大小相差超过1,将大根堆的堆头转移到小根堆的堆头
            min_heap.push(max_heap.top());
            max_heap.pop();
        }
    }

    double getMedian()
    {
        if (max_heap.size() + min_heap.size()&1)
            return max_heap.top();
        return (max_heap.top() + min_heap.top()) / 2.0;
    }
};

eg3:3492. 负载均衡 

 思想:小根堆

理解:设第i次任务开始时间为a[i],只有结束时间在a[i]之前才会影响算力是否够用

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 200010;
int n,m;
int s[N];//每台计算机算力
priority_queue<PII,vector<PII>,greater<PII> >q[N];//每台计算机任务
int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++ i)scanf("%d",&s[i]);//!下标从1开始
    while (m -- ){
        int a,b,c,d;
        scanf("%d%d%d%d",&a,&b,&c,&d);
        while(q[b].size()&&q[b].top().x<=a){//当堆尾的结束时刻小于当前分配时刻
            s[b] += q[b].top().y;//!恢复算力 
            q[b].pop();//弹出该任务
        }
        if(s[b]<d)puts("-1");//当前算力无法分配
        else{
            q[b].push({a+c,d});//加入任务
            s[b] -= d;//更新算力
            printf("%d\n",s[b]);//输出算力

        }
    }
    return 0;
}

11.哈希表

eg1:3779. 相等的和 

思想:快速完成增删改查操作------->哈希表 

思路:

  1. 先求出各个序列的和,再枚举去掉各个数之后和可能出现的情况,存入哈希表
  2.  若两个不同序列的和相同,则输出答案
#include <algorithm>
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 2e5 + 10;
typedef long long ll;
typedef pair<int, int> pii;
int w[N];
int main()
{
    int n;
    scanf("%d", &n);
    unordered_map<int, pii> s;
    for (int i = 1; i <= n; i++)
    {
        int m, sum = 0;
        scanf("%d", &m);
        for (int j = 1; j <= m; j++)
        {
            scanf("%d", &w[j]);
            sum += w[j];
        }
        for (int j = 1; j <= m; j++)
        {
            int t = sum - w[j];//当前序列能够得到的和有哪些
            if (s.count(t) && s[t].first != i)//哈希表中存在该和t,并且行号j和i不相同,说明找 
                                              //到了不在同一行的数
            {
                puts("YES");
                printf("%d %d\n", s[t].first, s[t].second);
                printf("%d %d\n", i, j);
                return 0;
            }
            s[t] = {i, j};//加入元素
        }
    }
    puts("NO");
    return 0;
}

12.DFS

eg1:5147. 数量

思路:暴力枚举(TLE)------->DFS 

思路:搜索该数的每一个数位,只有4和7两种选择

#include <algorithm>
#include <iostream>
using namespace std;
typedef long long ll;
int n;
int ans;
void dfs(ll u)//开long long防止爆int
{
    if (u>n)
        return;
    if(u)
        ans++;
    dfs(u * 10 + 4);
    dfs(u * 10 + 7);
}
int main()
{
    scanf("%d", &n);
    dfs(0);//从第0层开始搜索
    printf("%d", ans);
    return 0;
}

eg2:2005. 马蹄铁 

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 5;
int n;
int ans;
char g[N][N];//读入坐标数据
bool st[N][N];//判断该位置是否被走过
void dfs(int x, int y, int l, int r)
{
    st[x][y] = true;
    if (l == r)
    {
        ans = max(ans, l + r);
        st[x][y] = false;//回溯(还原现场)
        return;
    }
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};//表示坐标偏移量:上下左右
    for (int i = 0; i < 4; i++)
    {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 0 && a < n && b >= 0 && b < n && !st[a][b])
        {
            if (g[x][y] == ')' && g[a][b] == '(')//特判
                continue;
            if (g[a][b] == '(')
                dfs(a, b, l + 1, r);
            else
                dfs(a, b, l, r + 1);
        }
    }
    st[x][y] = false;//回溯(还原现场)
}
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++)
        scanf("%s", g[i]);
    if (g[0][0] == '(')
        dfs(0, 0, 1, 0);
    printf("%d", ans);
    return 0;
}

eg3:1613. 数独简单版 

法一:剪枝(时间复杂度低) 

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 10;
typedef long long ll;
char g[N][N];
bool row[N][N], col[N][N], cell[3][3][N];
bool dfs(int x, int y)
{
    if (y == 9)//搜到最后一列,返回第一列
        x++, y = 0;
    if (x == 9)
    {
         for (int i = 0; i < 9; i++)
            cout << g[i] << endl;
        return true;//这里返回true让下面for里面中间的dfs直接结束,不在回溯,少枚举很多情况
                    //并且是输出唯一解
    }
       
    if (g[x][y] != '.')
       return dfs(x, y + 1);
    for (int i = 0; i < 9; i++)
    {
        if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i])
        {
            g[x][y] = i + '1';
            row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
            if(dfs(x, y + 1)) return true;//剪枝,dfs返回值是true上面输出了答案,不用再回溯
                                          //并且这一枝递归直接结束。

            g[x][y] = '.';//回溯
            row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;//回溯
        }
    }
    return false;//如果某个方案失败,需要返回false让上面回溯
                 //不加这个也不会输出结果,因为如果这一枝递归没成功,返回false上面才能回溯
}
int main()
{
    for (int i = 0; i < 9; i++)
    {
        cin >> g[i];
        for (int j = 0; j < 9; j++)
        {
            if (g[i][j] != '.')
            {
                int t = g[i][j] - '1';
                row[i][t] = col[j][t] = cell[i / 3][j / 3][t] = true;
            }
        }
    }
    dfs(0, 0);
    return 0;
}

法二:未剪枝(时间复杂度高) 

bool dfs(int x, int y)
{
    if (y == 9) x ++, y = 0;
    if (x == 9)
    {
        for (int i = 0; i < 9; i ++ ) cout << g[i] << endl;
        return true;
    }

    if (g[x][y] != '.') return dfs(x, y + 1);

    for (int i = 0; i < 9; i ++ )
        if (!row[x][i] && !col[y][i] && !cell[x / 3][y / 3][i])
        {
            g[x][y] = i + '1';
            row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = true;
            dfs(x, y + 1);//不同点
            row[x][i] = col[y][i] = cell[x / 3][y / 3][i] = false;
            g[x][y] = '.';
        }
        //没有返回值
}

tips:改成void类型时没输出结果过原因 

错误代码  dfs(x, y+1)--------->如果当前位是数字,就直接往后搜,不能执行后面的for循环对该数进行赋值,否则就改变了该位置上原来的数值,也就搜不出结果

 ###也就是说,改成bool类型后,当g[x][y]!= '.'时,是直接dfs(x, y + 1)后面的for不执行,而void,是dfs(x, y + 1)后,后面那块for还执行

13.BFS

eg1:1101. 献给阿尔吉侬的花束

思路:求最短路-------->BFS

#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 210;
typedef pair<int, int> pii;
int n, m, r, c;
char g[N][N];
int d[N][N];
pii q[N * N];
int stx, sty;
int bfs()
{
    int hh = 0, tt = 0;
    q[0] = {stx, sty};//插入起点
    memset(d, -1, sizeof d);//初始化所有点都未走过
    d[stx][sty] = 0;//初始化
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    while (hh <= tt)
    {
        auto t = q[hh++];
        for (int i = 0; i < 4; i++)
        {
            int x = t.first + dx[i], y = t.second + dy[i];
            if (x >= 0 && x < r && y >= 0 && y < c && g[x][y] != '#' && d[x][y] == -1)
            {
                d[x][y] = d[t.first][t.second] + 1;
                if (g[x][y] == 'E')
                    return d[x][y];
                q[++tt] = {x, y};
            }
        }
    }
    return -1;
}
int main()
{
    cin >> n;
    while (n--)
    {
        cin >> r >> c;
        for (int i = 0; i < r; i++)
            for (int j = 0; j < c; j++)
            {
                cin >> g[i][j];
                if (g[i][j] == 'S')
                    stx = i, sty = j;
            }
        int t = bfs();
        if (t == -1)
            cout << "oop!" << endl;
        else
            cout << t << endl;
    }
    return 0;
}

14.树与图遍历

eg1: 3699. 树的高度

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int res = 0;
int h[N], e[N * 2], ne[N * 2], idx, d[N];//无向图---->开两倍
bool st[N];
void add(int a, int b)//存储边
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
    st[u] = true;
    res = max(res, d[u]);
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            d[j] = d[u] + 1;
            dfs(j);
        }
    }
}
int main()
{
    idx = 0;
    memset(h, -1, sizeof h);
    cin >> n >> m;
    d[m] = 0;
    for (int i = 0; i < n - 1; i++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);//无向图
    }
    dfs(m);
    cout << res << endl;
    return 0;
}

15.拓扑排序

tips:有向无环图<-------->拓扑序列 

eg1:3704. 排队

思路:

  1. 每个要求包含两个整数 a,b,表示小朋友 a 要排在小朋友 b 的前面。--->拓扑排序
  2. 当符合条件的排队顺序不唯一时,编号更小的小朋友尽量更靠前。---->优先队列 
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 510, M = 5010;
int n, m;
int h[N], e[M], ne[M], idx, d[N];//d[i]存i的入度
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void topsort()
{
    priority_queue<int, vector<int>, greater<int>> heap;//优先队列
    for (int i = 1; i <= n; i++)
        if (!d[i])
            heap.push(i);
        while (heap.size())
        {
            auto t = heap.top();
            printf("%d ", t);
            heap.pop();
            for (int i = h[t]; i != -1; i = ne[i])
            {
                int j = e[i];
                if (--d[j] == 0)
                    heap.push(j);
            }
        }
}
int main()
{
    cin >> n >> m;
    memset(h,-1,sizeof h);
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        d[b]++;//a在b的前面,a-->b,b的入度+1
        add(a, b);
    }
    topsort();
    return 0;
}

16.最短路 

eg1:4275. Dijkstra序列 

思想:朴素版Dijkstra 

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int g[N][N], dist[N];
bool st[N];
int q[N];
bool dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    memset(st,0,sizeof st);
    dist[q[0]] = 0;
    for (int i = 0; i < n;i++)
    {
        int t = q[i];
        for (int j = 1; j <= n;j++)
        {
            if(!st[j]&&dist[t]>dist[j])
                return false;
        }
        for (int j = 1; j <= n;j++)
            dist[j] = min(dist[j], dist[t] + g[t][j]);
        st[t] = true;
    }
    return true;
}
int main()
{
    scanf("%d%d", &n, &m);
    memset(g,0x3f,sizeof g);
    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = g[b][a] = c;
    }
    int k;
    scanf("%d", &k);
    while(k--)
    {
        for (int i = 0; i < n;i++)
            scanf("%d", &q[i]);
        if(dijkstra())
            puts("Yes");
        else
            puts("No");
    }
    return 0;
}

eg2: 4196. 最短路径

思想:spfa 

#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10;
typedef long long ll;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int pre[N];//记录路径
bool st[N];
ll dist[N];
int path[N];//输出路径
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa()//从起点遍历到终点
{
    memset(dist, 0x3f, sizeof dist);//初始化
    memset(pre, -1, sizeof pre);//初始化
    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];
                pre[j] = t;//记录路径
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
}
int main()
{
    scanf("%d%d", &n, &m);
    memset(h,-1,sizeof h);//初始化邻接表
    while (m--)
    {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        add(a, b, w), add(b, a, w);
    }
    spfa();
    if (pre[n] == -1)
        puts("-1");
    else
    {
        int cnt = 0;
        for (int i = n; i != -1; i = pre[i])//从终点向起点存储路径
            path[cnt++] = i;
        for (int i = cnt - 1; i >= 0; i--)//逆序输出路径
            printf("%d ", path[i]);
    }
    return 0;
}

eg3:1488. 最短距离 

思想:超级源点+ 堆优化版Dijkstra

超级源点跟超级汇点是模拟出来的虚拟点,多用于图中:
<1>同时有多个源点和多个汇点,建立超级源点和超级汇点

<2>同时有多个源点和一个汇点,建立超级源点

<3>同时有多个汇点和一个源点,建立超级汇点

思路:

  1. 本题为多个源点的最短路问题-------->对每个源点使用Dijkstra(TLE)
  2. 优化:新增一个点(超级源点),超级源点连向所有商店一条边,权值为0,再从超级源点做单源最短路。

#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
const int N = 1e5 + 10, M = 3e5 + 10;//无向图要开N的(2倍)+新增了超级源点连向各源点的边(1倍)
typedef pair<int, int> pii;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
bool st[N];
int dist[N];
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[0] = 0;//超级源点的下标为0,从超级源点开始做单源最短路
    priority_queue<pii, vector<pii>, greater<pii>> heap;
    heap.push({0, 0});//同步修改
    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});
            }
        }
    }
}
int main()
{
    scanf("%d%d", &n, &m);
    memset(h,-1,sizeof h);
    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    int k;
    scanf("%d",&k);
    while (k--)
    {
        int x;
        scanf("%d", &x);
        add(0, x, 0);
    }
    dijkstra();
    int q;
    scanf("%d",&q);
    while(q--)
    {
        int y;
        scanf("%d", &y);
        printf("%d\n", dist[y]);
    }
    return 0;
}

eg4:4872. 最短路之和 

思想:Floyd

小技巧:在图中删点不好做,逆向思维,把点一个一个加入图中,
每次以第 i个点为中间点做 Floyd,最后把所有加入的点两两之间距离的和求出即可。

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 510;
typedef long long ll;
int n;
bool st[N];//判断点是否在图中
int d[N][N];
int p[N];
ll ans[N];
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            scanf("%d", &d[i][j]);
    for (int i = 1; i <= n;i++)
        scanf("%d", &p[i]);
    for (int u = n; u > 0;u--)//向图中加入点
    {
        int k = p[u];
        st[k] = true;
        for (int i = 1; i <= n;i++)//floyd
            for (int j = 1; j <= n;j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
        for (int i = 1; i <= n;i++)
            for (int j = 1; j <= n;j++)
            if(st[i]&&st[j])
                ans[u] += d[i][j];
    }
    for (int i = 1; i <= n;i++)
            printf("%lld ", ans[i]);
    return 0;
}

17.最小生成树

eg1:346. 走廊泼水节

思想:Kruskal 

思路:用Kruskal 算法构建最小生成树

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 6010;
int n, m;
int p[N], cnt[N];
struct Edge
{
    int a, b, w;
    bool operator < (const Edge &W) const
    {
        return w < W.w;
    }
} edges[N];
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
int Kruskal()
{
    sort(edges, edges + n - 1);
    for (int i = 1; i <= n; i++)
        p[i] = i, cnt[i] = 1;
    int ans = 0;
    for (int i = 0; i < n - 1; 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;
            ans += (cnt[a] * cnt[b] - 1) * (w + 1);
            cnt[b] += cnt[a];
        }
    }
    return ans;
}
int main()
{
    int T;
    scanf("%d",&T);
    while (T--)
    {
        scanf("%d", &n);
        for (int i = 0; i < n - 1; i++)
        {
            int x, y, z;
            scanf("%d%d%d", &x, &y, &z);
            edges[i] = {x, y, z};
        }
        printf("%d\n", Kruskal());
    }
    return 0;
}

eg2:3728. 城市通电 

18.二分图

eg1:257. 关押罪犯

 思想:染色法判定二分图

思路: 

#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 20010, M = 200010;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int color[N];
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool dfs(int u,int c,int limit)
{
    color[u] = c;
    for (int i = h[u]; i != -1;i=ne[i])
    {
        if(w[i]<=limit)//该点的权重<=limit,不能构成二分图,继续向下遍历
            continue;
        int j = e[i];
        if(color[j]==-1)
        {
            if(!dfs(j,!c,limit))
                return false;
        }
        else if (color[j]==c)
            return false;
    }
    return true;
}
bool check(int limit)
{
    memset(color, -1, sizeof color);
    bool flag = true;
    for (int i = 1; i <= n;i++)
    {
        if(color[i]==-1)
        if(!dfs(i,0,limit))
        {
            flag = false;
            break;
        }
    }
    return flag;
}
int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    int l = 0, r = 1e9;
    while (l < r)
    {
        int mid = l + r >> 1;
        if(check(mid))
            r = mid;
        else
            l = mid + 1;
    }
    printf("%d", l);
    return 0;
}

eg2:4205. 树的增边

思想:染色法判定二分图(所有的树都是二分图)

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1e5+10, M = 2*N;
typedef long long ll;
int n, m;
int h[N], e[M], ne[M], idx;
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u, int c, int f)
{
    if (!c)//若u这个点为另外一种颜色
        m++;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(j==f)//保证一直向下  不可以向上
            continue;
        dfs(j, !c, u);// 改变颜色 当前点变为父节点
    }
}
int main()
{
    scanf("%d", &n);
    memset( h, -1, sizeof h );
    for (int i = 1; i <= n - 1; i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);//二分图为无向图
    }
    dfs(1, 0, 0);//遍历树(假设第一个点为白色)
    printf("%lld", (ll)m * (n - m) - (n - 1));
    return 0;
}

eg3: 1394. 完美牛棚

思想:最大匹配问题------>匈牙利算法

#include <algorithm>
#include <iostream>
using namespace std;
const int N = 40010;//邻接表存边,200个点,每个点最多200条边(200*200)
int n, m;
int h[N], e[N], ne[N], idx;
int match[N];
bool st[N];
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int x)//匈牙利算法
{
    for (int i = h[x]; i != -1;i=ne[i])
    {
        int j = e[i];
        if(!st[j])
        {
            st[j] = true;
            if(match[j]==0||find(match[j]))
            {
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}
int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof(h));
    for (int i = 1; i <= n; i++)
    {
        int cnt;
        scanf("%d", &cnt);
        while (cnt--)
        {
            int x;
            scanf("%d", &x);
            add(i, x);
        }
    }
    int res = 0;
    for (int i = 1; i <= n;i++)
    {
        memset(st, false, sizeof st);
        if(find(i))
            res++;
    }
    printf("%d", res);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值