大二寒假第六周学习笔记

本文探讨了多项编程竞赛中常见的算法和数据结构问题,包括区间DP、最短路、整数三分等策略。通过实例解析,展示了如何运用动态规划、二分查找、贪心思想解决复杂问题,并提供了优化技巧,如暴力优化和三分优化。同时,文章强调了理解题目本质和预处理数据的重要性。
摘要由CSDN通过智能技术生成

周四

E. Array Shrinking(区间dp)

一开始往贪心的方面想,结果WA了

其实提示比较明显了,n只有500,那显然是n^3的时间复杂度

同时又有合并操作,那显然是区间dp了

开一个辅助数组g,表示l到r合并后的数字是什么

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 500 + 10;
int dp[N][N], g[N][N], n;

int main()
{
    scanf("%d", &n);
    memset(dp, 0x3f, sizeof dp);
    _for(i, 1, n) scanf("%d", &g[i][i]), dp[i][i] = 1;
    _for(len, 2, n)
        _for(l, 1, n)
        {
            int r = l + len - 1;
            if(r > n) break;
            _for(k, l, r - 1)
            {
                if(dp[l][k] == 1 && dp[k + 1][r] == 1 && g[l][k] == g[k + 1][r])
                {
                    dp[l][r] = 1;
                    g[l][r] = g[l][k] + 1;
                }
                else dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
            }
        }
    printf("%d\n", dp[1][n]);

    return 0;
}

E. Binary Subsequence Rotation(思维)

首先转化一下,一些位置需要从1变成0,看作+,一些需要从0变成1,看作-

一次操作可以抵消一个-+-+-+,即正负交替的序列

那最少需要多少次操作呢

我是这么想的,和题解稍微不太一样

用两个变量记录当前以正为结尾的序列个数和以负为结尾的序列个数,维护一下即可

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 1e6 + 10;
int a[N], b[N], cnta, cntb, n;

int main()
{
    scanf("%d", &n);
    _for(i, 1, n) scanf("%1d", &a[i]), cnta += (a[i] == 1);
    _for(i, 1, n) scanf("%1d", &b[i]), cntb += (b[i] == 1);

    if(cnta != cntb)
    {
        puts("-1");
        return 0;
    }

    int cur0 = 0, cur1 = 0;   //0表示以正为结尾的序列  1表示以负为结尾的序列
    _for(i, 1, n)
    {
        if(a[i] && !b[i]) //正
        {
            if(cur1) cur1--, cur0++;
            else cur0++;
        }
        else if(!a[i] && b[i])
        {
            if(cur0) cur0--, cur1++;
            else cur1++;
        }
    }
    printf("%d\n", cur0 + cur1);

    return 0;
}

D. Coloring Edges(dfs树)

加深了对dfs树的理解

dfs树上有三种边,树边,横叉边,返祖边。

和无向图不同,有向图如果访问到了已经访问过的节点,不一定成环,要分两种情况。

设u为当前节点,v为访问到的之前已经访问过的节点

如果v可以到u,那么可以成环,这是一条返祖边。

否则不能成环,这是一条横叉边

如果判断呢,只需判断v是否还在栈里面,如果还在那就是返祖边。

对于这道题,可能成环的只有返祖边,那就把返祖边全部染成2,返祖边之间不可能成环(画一画) 所以是可行的。

还有一种方法,先判环,如果有环的,那就把标号大的到标号小的染成1,否则染成2,这样一定不会成环,也挺妙的,是一个全局的思考方法。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 5000 + 10;
struct Edge{ int v, id; };
vector<Edge> g[N];
int c[N], vis[N], n, m, ans = 1;

void dfs(int u)
{
    vis[u] = 1;
    for(auto x: g[u])
    {
        int v = x.v, cur = x.id;
        if(c[cur]) continue;
        if(vis[v] == 1) c[cur] = ans = 2;
        else c[cur] = 1;
        if(!vis[v]) dfs(v);
    }
    vis[u] = -1;
}

int main()
{
    scanf("%d%d", &n, &m);
    _for(i, 1, m)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back({v, i});
    }
    _for(i, 1, n) dfs(i);

    printf("%d\n", ans);
    _for(i, 1, m) printf("%d ", c[i]);

    return 0;
}

E. Weights Distributing(最短路)

这道题我一直卡在两条路径重合该怎么处理

其实解法非常暴力,直接暴力枚举两条路径相交的点是哪一点就可以了

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 2e5 + 10;
int da[N], db[N], dc[N];
vector<int> g[N];
int n, m, a, b, c;
ll w[N];

void bfs(int s, int d[])
{
    _for(i, 1, n) d[i] = 1e9;
    d[s] = 0;
    queue<int> q;
    q.push(s);
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        for(int v: g[u])
            if(d[u] + 1 < d[v])
            {
                d[v] = d[u] + 1;
                q.push(v);
            }
    }
}

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d%d%d%d", &n, &m, &a, &b, &c);
        _for(i, 1, m) scanf("%lld", &w[i]);
        sort(w + 1, w + m + 1);
        _for(i, 2, m) w[i] += w[i - 1];
        _for(i, 1, n) g[i].clear();
        _for(i, 1, m)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            g[u].push_back(v);
            g[v].push_back(u);
        }

        bfs(a, da);
        bfs(b, db);
        bfs(c, dc);

        ll ans = 1e18;
        _for(i, 1, n)
        {
            if(da[i] + db[i] + dc[i] > m) continue;
            ans = min(ans, w[db[i]] + w[da[i] + db[i] + dc[i]]);
        }
        printf("%lld\n", ans);
    }

    return 0;
}

D. The Number of Pairs(欧拉筛)

1e7的数据很可能是给你质因数分解的

推了一下公式,发现最后要求一个数的质因子种类个数

第一时间想到的是欧拉筛预处理出每个数的最小质因数,然后对于一个数可以log的时间内暴力质因数分解得出答案

这么写了一发,AC了

之后看其他人的题解发现有更好的做法,在欧拉筛的时候直接预处理出来,看代码。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int M = 2e7 + 10;
int cnt[M];
vector<int> p;
bool vis[M];

void get_prime()
{
    vis[0] = vis[1] = true;
    _for(i, 2, 2e7)
    {
        if(!vis[i]) p.push_back(i), cnt[i] = 1;
        for(int x: p)
        {
            if(i * x > 2e7) break;
            vis[i * x] = true;
            cnt[i * x] = cnt[i] + 1;   //多了x这个质因子
            if(i % x == 0)
            {
                cnt[i * x] = cnt[i];   //没有多x这个质因子
                break;
            }
        }
    }
}

int solve(int c, int d, int x)
{
    x += d;                       //坑,这里会使得最大值为2e7而不是1e7
    if(x % c != 0) return 0;
    return 1 << cnt[x / c];      //小范围可以不预处理,直接这么写
}

int main()
{
    get_prime();

    int T; scanf("%d", &T);
    while(T--)
    {
        ll ans = 0;
        int c, d, x;
        scanf("%d%d%d", &c, &d, &x);
        for(int i = 1; i * i <= x; i++)
            if(x % i == 0)
            {
                ans += solve(c, d, i);
                if(i * i != x) ans += solve(c, d, x / i);
            }
        printf("%lld\n", ans);
    }

    return 0;
}

周五

E. Restorer Distance(整数型三分)

这道题一开始我就感觉是三分,但是不太确定是不是单峰函数,而且也没写过整数型的三分,只写过几次浮点数的三分。

其实不确定的话暴力枚举一下就可以了。

对于一个确定的高度,要将第三种操作预处理一下,贪心。

整数型三分的思路也是和二分很像,可以看代码。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
const int N = 1e5 + 10;
int h[N], a, b, c, n;

ll check(int key)
{
    ll cnta = 0, cntb = 0;
    _for(i, 1, n)
    {
        if(h[i] < key) cnta += key - h[i];
        if(h[i] > key) cntb += h[i] - key;
    }
    ll t = min(cnta, cntb);
    cnta -= t; cntb -= t;
    return cnta * a + cntb * b + t * c;
}

int main()
{
    scanf("%d%d%d%d", &n, &a, &b, &c);
    _for(i, 1, n) scanf("%d", &h[i]);
    c = min(c, a + b);

    int l = 0, r = 2e9;
    while(l + 10 < r)   //把l和r限制在一个长度为10的区间内
    {
        int lmid = l + (r - l) / 3, rmid = r - (r - l) / 3;
        if(check(lmid) < check(rmid)) r = rmid;
        else l = lmid;
    }
    ll ans = 1e18;
    _for(i, l, r) ans = min(ans, check(i));  //在这个区间内取最值
    printf("%lld\n", ans);

    return 0;
}

F. Swaps Again(猜结论)

这种题没什么技巧,就是猜结论。显然是要判断交换很多次有什么性质,就自己交换多次猜性质。

通过可以观察到原来是对称的,变换之后也一定是对称的。

要证明要很容易想,可以发现题目的这种交换等价于将两个区间镜像对称,然后再将两个区间自己左右对称一下,在这两个操作的情况下,一组对称的数变换后还是对称的。

既然有这个性质,就很好判断了。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 500 + 10;
int a[N], b[N], n;

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%d", &n);
        _for(i, 1, n) scanf("%d", &a[i]);
        _for(i, 1, n) scanf("%d", &b[i]);

        if(n % 2 == 1 && a[(n + 1) / 2] != b[(n + 1) / 2])
        {
            puts("No");
            continue;
        }

        vector<pair<int, int>> v1, v2;
        _for(i, 1, n / 2)
        {
            v1.push_back({min(a[i], a[n - i + 1]), max(a[i], a[n - i + 1])});
            v2.push_back({min(b[i], b[n - i + 1]), max(b[i], b[n - i + 1])});
        }
        sort(v1.begin(), v1.end());
        sort(v2.begin(), v2.end());
        puts(v1 == v2 ? "Yes" : "No");
    }

    return 0;
}

D. GCD of an Array(动态开点线段树/multiset)

这道题很容易想到每个质数开一颗线段树,维护最小值,因为空间很大,所以要动态开点就行了。

每次修改,都会改变当前质数对gcd的贡献,所以先乘上逆元去掉原来的贡献,然后修改,再乘上现在的贡献。

动态开点也不难,也就是几个地方改一改就行,开ls rs root数组,然后建立新点,也就是这样

空间能开多大开多大

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
const int M = 2e7;
const int mod = 1e9 + 7;
int vis[N], k[N], n, q, ans = 1;
int t[M], ls[M], rs[M], root[N], cnt;
vector<int> p;

void init()
{
    for(int i = 2; i <= 2e5; i++)
    {
        if(!vis[i]) p.push_back(i), k[i] = i;
        for(int x: p)
        {
            if(x * i > 2e5) break;
            vis[x * i] = 1;
            k[x * i] = x;
            if(i % x == 0) break;
        }
    }
}

int mul(int a, int b) { return 1LL * a * b % mod; }

int binpow(int a, int b)
{
    int res = 1;
    for(; b; b >>= 1)
    {
        if(b & 1) res = mul(res, a);
        a = mul(a, a);
    }
    return res;
}

int inv(int x) { return binpow(x, mod - 2); }

void up(int k)
{
    t[k] = min(t[ls[k]], t[rs[k]]);
}

void modify(int& k, int l, int r, int x, int val)
{
    if(!k) k = ++cnt; 
    if(l == r)
    {
        t[k] += val;
        return;
    }
    int m = l + r >> 1;
    if(x <= m) modify(ls[k], l, m, x, val);
    else modify(rs[k], m + 1, r, x, val);
    up(k);
}

void solve(int i, int x)
{
    while(x > 1)
    {
        int p = k[x], num = 0;
        while(x % p == 0) x /= p, num++;
        ans = mul(ans, inv(binpow(p, t[root[p]])));
        modify(root[p], 1, n, i, num);
        ans = mul(ans, binpow(p, t[root[p]]));
    }
}

int main()
{
    init();
    scanf("%d%d", &n, &q);
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        solve(i, x);
    }

    while(q--)
    {
        int i, x;
        scanf("%d%d", &i, &x);
        solve(i, x);
        printf("%d\n", ans);
    }

    return 0;
}

因为这道题要用数据结构维护一堆数的最小值以及修改操作,这个除了动态开点线段树,还可以用map + multiset实现。不过写起来比线段树麻烦一点,线段树比较暴力。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
const int mod = 1e9 + 7;
unordered_map<int, int> mp[N];
multiset<int> s[N];
int vis[N], k[N], n, q, ans = 1;
vector<int> p;

void init()
{
    for(int i = 2; i <= 2e5; i++)
    {
        if(!vis[i]) p.push_back(i), k[i] = i;
        for(int x: p)
        {
            if(x * i > 2e5) break;
            vis[x * i] = 1;
            k[x * i] = x;
            if(i % x == 0) break;
        }
    }
}

int mul(int a, int b) { return 1LL * a * b % mod; }

int binpow(int a, int b)
{
    int res = 1;
    for(; b; b >>= 1)
    {
        if(b & 1) res = mul(res, a);
        a = mul(a, a);
    }
    return res;
}

int inv(int x) { return binpow(x, mod - 2); }

void add(int i, int p, int cnt)
{
    if(!mp[i][p])
    {
        mp[i][p] = cnt;
        s[p].insert(cnt);
        if(s[p].size() == n) ans = mul(ans, binpow(p, *s[p].begin()));
    }
    else
    {
        if(s[p].size() == n) ans = mul(ans, inv(binpow(p, *s[p].begin())));
        s[p].erase(s[p].find(mp[i][p]));
        mp[i][p] += cnt;
        s[p].insert(mp[i][p]);
        if(s[p].size() == n) ans = mul(ans, binpow(p, *s[p].begin()));
    }
}

void solve(int i, int x)
{
    while(x > 1)
    {
        int t = k[x], cnt = 0;
        while(x % t == 0) x /= t, cnt++;
        add(i, t, cnt);
    }
}

int main()
{
    init();
    scanf("%d%d", &n, &q);
    _for(i, 1, n)
    {
        int x; scanf("%d", &x);
        solve(i, x);
    }

    while(q--)
    {
        int i, x;
        scanf("%d%d", &i, &x);
        solve(i, x);
        printf("%d\n", ans);
    }

    return 0;
}

周六

E. Connected Components?(暴力优化)

这题秒啊,非常思维的一道题。

求连通块显然是并查集或者搜索,但显然会T

这道题用搜索求,想想怎么优化。

如果直接搜索的话,复杂度是n方的。关键是优化找边的这个for循环

有些点已经搜过的了,但是以后每次都要遍历这个点,就会很慢。

那么维护一个未访问点的点集,每次访问一个点就把它从点集里面删掉。

每次找边的时候从未访问的点集里面找。

用vector配合pop_back实现。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

const int N = 2e5 + 10;
map<int, int> g[N];
int n, m;
vector<int> ans, node;

int bfs(int u)
{
    int res = 0;
    queue<int> q;
    q.push(u);
    node.pop_back();
    while(!q.empty())
    {
        res++;
        int u = q.front(); q.pop();
        rep(i, 0, node.size())
        {
            int v = node[i];
            if(g[u][v]) continue;
            q.push(v);
            swap(node[i], node[node.size() - 1]);
            node.pop_back();
            i--;
        }
    }
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    while(m--)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u][v] = g[v][u] = 1;
    }

    _for(i, 1, n) node.push_back(i);
    while(!node.empty())
    {
        int u = node.back();
        ans.push_back(bfs(u));
    }
    sort(ans.begin(), ans.end());
    printf("%d\n", ans.size());
    for(int x: ans) printf("%d ", x);

    return 0;
}

G. Gift Set(三分/二分)

一.三分做法

官方题解和网上题解都是二分,有人的三分被hack了,其实三分是可以过的,我自己独立想的时候就是用三分AC的

可以打一下表,发现是单峰的,但是问题在于可以发现会出现很长一段区间的答案是相同的情况。

这样子在check(lmid) = check(rmid)相等的时候就不好判断了。这个问题在浮点数的时候是不会出现的。

一般的题也不会出现,顶多相邻几个答案相同,这种情况可以把l和r限制在一个区间内,这样lmid

和rmid的距离比较长,可以避免这种情况。

那怎么解决这个问题呢,用浮点数,浮点数就不会有相等的这个问题,所以可以先当作浮点数求出一个答案,然后再取整回整数,在这个整数附近取最大值。

想到这个方法的前提是知道为什么直接三分会WA,理解深刻。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
ll x, y, a, b;

double check(double k1)
{
    double xx = x, yy = y;
    xx -= a * k1; yy -= b * k1;
    if(xx < 0 || yy < 0) return 0;
    return k1 + min(xx / b, yy / a);
}

int check2(int k1)
{
    ll xx = x, yy = y;
    xx -= a * k1; yy -= b * k1;
    if(xx < 0 || yy < 0) return 0;
    return k1 + min(xx / b, yy / a);
}

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%lld%lld%lld%lld", &x, &y, &a, &b);
        double l = 0, r = 1e9 + 1;
        while(l + 0.1 < r)
        {
            double lmid = l + (r - l) / 3, rmid = r - (r - l) / 3;
            if(check(lmid) < check(rmid)) l = lmid;
            else r = rmid;
        }
        int ans = (int)l;
        printf("%d\n", max(check2(max(ans - 1, 0)), max(check2(ans), check2(ans + 1))));
    }

    return 0;
}

二.二分做法

首先答案具有单调性

那么在确定答案的情况下,可以推出4个不等式,满足这4个不等式即可。

这有个细节,就是取整的问题。

我一开始是用平常整数那样取整的,但是其实那是基础正数的前提下。

如果有负数的话就不对了,于是就转化成浮点数来取整。

#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i < (b); i++)
#define _for(i, a, b) for(int i = (a); i <= (b); i++)
using namespace std;

typedef long long ll;
ll x, y, a, b;

bool check(int ans)
{
    ll t1 = ceil(double(y - a * ans) / (b - a));
    ll t2 = floor(double(x - b * ans) / (a - b));
    return max(t1, 0ll) <= min(t2, (ll)ans);
}

int main()
{
    int T; scanf("%d", &T);
    while(T--)
    {
        scanf("%lld%lld%lld%lld", &x, &y, &a, &b);
        if(a == b)
        {
            printf("%lld\n", min(x, y) / a);
            continue;
        }
        if(a < b) swap(a, b);

        int l = 0, r = 1e9 + 1;
        while(l + 1 < r)
        {
            int m = l + r >> 1;
            if(check(m)) l = m;
            else r = m;
        }
        printf("%d\n", l);
    }

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值