牛客小白月赛45 题解

A题 悬崖 (脑筋急转弯)

脑筋急转弯题,就不翻译题目了,没必要。

\(n\leq x\) 时,可以一直跳到两个墙合并,答案为 \(nx\)

\(n>x\) 时,至多跳一次就掉下去,但是还是有跳跃距离的,答案为 \(x\)

#include<bits/stdc++.h>
using namespace std;
int main()
{
    long long x, n, ans;
    cin >> x >> n;
    ans = n > x ? x : n * x;
    cout << ans << endl;
    return 0;
}

B题 数数 (签到)

给定一个函数:

void dfs(int cnt) { //cnt从1开始 如同dfs(1)
 for(int i = 1;i <= cnt;i++) ans++;
 dfs(cnt + 2);
}

问函数递归到第 \(n\) 层时,\(ans\) 的值为多少?(从 \(dfs(1)\) 开始,\(ans\) 是全局变量,初始值为 0)

样例:\(n=2\) 时,\(ans=4\)

\(0\leq n\leq 10^9\)

不难发现,第 \(i\) 层会让答案增加 \(2i-1\),所以 \(ans=\sum\limits_{i=1}^n2i-1=n^2\)

#include<bits/stdc++.h>
using namespace std;
int main()
{
    long long n;
    cin >> n;
    cout << n * n;
    return 0;
}

C题 山楂 (数学,贪心)

已知现在有不少等级不同的糖果,我们可以每次选择将 3 或 4 个 \(i\) 级的糖果合成为一个 \(i+1\) 级的糖果,并获得 \(xi\) 点积分。当若干个 8 级糖果被合成为一个 9 级糖果后,这个糖果会消失,不再能够被继续合成。

现在给定前八级糖果的数量 \(a_1,a_2,\cdots,a_8\),问我们能够合成的最高积分是多少?

\(0\leq a_i\leq 10^9\)

糖果只能从低级向高级合成,所以我们贪心的考虑,先低级后高级。

我们假设 \(k\) 级糖果的数量为 \(n\),那么 \(n<3\) 时无法合成,\(n=3\) 时可以将三个糖果合并,\(n=4,5\) 时则将四个全部合并。

\(n\geq 6\) 时候,有一个不是很显然的性质:\(n\) 一定能够被表示为 \(3x+4y(x,y\geq 0)\) 的形式(可以同余证明,或者去参考 小凯的疑惑),这意味着这一级的糖果必然能被全部合成,即给积分贡献 \(nk\)。那么,我们必然要最大化合成的 \(k+1\) 级糖果数量,显然数量为 \(\lfloor\frac{n}{3}\rfloor\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL a[10];
const LL v[6] = {0, 0, 0, 3, 4, 4};
int main()
{
    for (int i = 1; i <= 8; ++i)
        cin >> a[i];
    LL ans = 0;
    for (int k = 1; k <= 8; ++k) {
        LL n = a[k];
        ans += k * (n > 5 ? n : v[n]);
        a[k + 1] += n / 3;
    }
    cout << ans;
    return 0;
}

D题 切糕 (思维,前缀和)

给定一个括号串,可以可以将其切上几刀或者不切,将其变为若干个合法括号串,问有多少种切的方式?

合法括号串:串内左右括号数量相等,且任意前缀内左括号数量不少于右括号。

\(|s|\leq 10^6\),答案对 \(10^9+7\) 取模

若干个合法括号串的拼接必然也是一个合法括号串,所以我们直接对原括号串扫一遍,看看合不合法。

如果合法,我们重新开始,每找到能切的地方就标记一下,最后记标记的总数量为 \(cnt\),那么最后答案就是 \(2^{cnt}\)

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1000010;
const LL mod = 1e9 + 7;
int n = 0, a[N], sum[N];
int main()
{
    //read
    string s;
    cin >> s;
    for (char c : s) a[++n] = c == '(' ? 1 : -1;
    //solve
    bool NoSolution = false;
    int cnt = 0;
    for (int i = 1; i <= n; ++i) {
        sum[i] = sum[i - 1] + a[i];
        if (sum[i] == 0 && i != n) cnt++;
        if (sum[i] < 0) NoSolution = true;
    }
    if (sum[n] != 0) NoSolution = true;
    if (NoSolution) {
        cout << -1;
        return 0;
    }
    LL ans = 1;
    for (int i = 0; i < cnt; ++i)
        ans = ans * 2 % mod;
    cout << ans;
    return 0;
}

E题 筑巢 (树形DP)

给定一棵树,树上的点和边都拥有一个舒适值

现在想要在树上选择一个连通块,使得连通块内部的点和边的舒适值之和最大。

\(n\leq 10^5,|w|\leq 10^9\)

我们考虑一手树形 DP,从根节点 1 开始,那么显然,这个连通块要么包含自己,要么在它的某一棵子树中。

那么,我们尝试设立状态 \(dp_{x,0/1}\),表示点 \(x\) 在选或者不选下的所得连通块最大值,那么有 DP 方程:

\[dp_{x,0}=\max\limits_{y\in sun(x)}\max(dp_{y,0},dp_{y,1}) \\ dp_{x,1}=w_x+\sum\limits_{(y,v)\in sun(x)}\max(0,dp_{y,1}) \]

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;

int n, w[N];
struct Edge { int to, val; };
vector<Edge> tree[N];
LL dp[N][2];
void dfs(int x, int fa) {
    dp[x][0] = -1e18, dp[x][1] = w[x];
    for (Edge e : tree[x]) {
        int y = e.to, v = e.val;
        if (y != fa) {
            dfs(y, x);
            dp[x][0] = max(dp[x][0], max(dp[y][0], dp[y][1]));
            dp[x][1] = dp[x][1] + max(dp[y][1] + v, 0LL);
        }
    }
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> w[i];
    for (int i = 1; i < n; ++i) {
        int x, y, v;
        cin >> x >> y >> v;
        tree[x].push_back((Edge){y, v});
        tree[y].push_back((Edge){x, v});
    }
    dfs(1, 0);
    cout << max(dp[1][0], dp[1][1]);
    return 0;
}

F题 交换

题面很详细且简洁,就不翻译了。

我们记排列长度 \(p=10\)

对于每次询问,我们考虑枚举所有可能的区间并进行操作,总复杂度为 \(O(mn^3p)\)

我们参考这场的前缀和训练题 牛牛的猜球游戏,来对区间操作进行优化,可以将复杂度降到 \([mn^2p]\)。不过显然,枚举的复杂度降不了了,必须得进行优化。

(我一开始以为这个答案长度可以二分,等比赛结束了才发现这个不符合二分性质)

注意到一个性质:对于 \(p=10\),排列的所有可能仅有 \(10!=3628800\) 种,这意味着我们可以直接从排列中枚举,将复杂度降到 \(O(mp!p)\)。但问题是, \(n=2*10^3\) 的规模,\(n^2\)\(p!\) 是一个数量级的,也没啥用啊?

我们换一个角度考虑,在每次询问中,我们已经知道了原排列,需要将其变为一个按照升序来的新排列,那么,我们不难构造出这个操作序列是咋样的,之后直接在所有排列中查找这个操作序列是否存在即可。

Trie树

Trie树是为了实现字符串的快速检索,同样的,它也可以实现排列的检索。

不过这个Trie树应该开多大是应该先算好的(毕竟空间有限),我们可以,手建一棵排列长度为 4 的Trie树,计算一下大小看看。

我们记一个排列长度为 \(k\) 的Trie树的大小为 \(f(k)\)(按照节点数的数量来算),找规律发现:\(f(k)=kf(k-1)+1,f(1)=2\)

这是一个阶乘增长的函数(查表发现 \(f(n)=\sum\limits_{k=0}^n\dfrac{n!}{k!}\)),递推可得 \(f(10)=9864101\),就离谱(我们得开一个将近 \(10^8\) 规模的 int 数组,还好内存限制是 512M,再加上很多叶子节点不需要向下扩展,所以实际上很多空间是未被使用的,所以不会被记入使用范围)

#include<bits/stdc++.h>
using namespace std;
const int N = 2010, SIZE = 1e7 + 10;
struct State {
    int a[11];
    State(){}
    State(int k, int *arr) {
        for (int i = 1; i <= k; ++i) a[i] = arr[i];
        for (int i = k + 1; i <= 10; ++i) a[i] = i;
    }
    void Swap(int x, int y) { swap(a[x], a[y]); }
};
//
int Trie[SIZE][11], V[SIZE], tot = 0;
void insert(State s, int val) {
    int p = 0, *arr = s.a;
    for (int i = 1; i <= 10; ++i) {
        int x = arr[i];
        if (!Trie[p][x]) Trie[p][x] = ++tot, V[tot] = val;
        p = Trie[p][x];
        V[p] = min(V[p], val);
    }
}
int search(int k, State s) {
    int p = 0, *arr = s.a;
    for (int i = 1; i <= k; ++i) {
        p = Trie[p][arr[i]];
        if (!p) return -1;
    }
    return V[p];
}
int n, m, x[N], y[N];
State opt[N][2];
int main()
{
    //read
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%d%d", &x[i], &y[i]);
    //init
    for (int i = 1; i <= 10; ++i)
        opt[0][0].a[i] = i;
    insert(opt[0][0], 0);
    for (int i = 1; i <= n + 1; ++i)
        opt[i][0] = opt[0][0];
    for (int len = 1; len <= n; ++len) {
        for (int i = 1; i + len - 1 <= n; ++i) {
            opt[i][len % 2] = opt[i + 1][(len - 1) % 2];
            opt[i][len % 2].Swap(x[i], y[i]);
            insert(opt[i][len % 2], len);
        }
    }
    //query
    while (m--) {
        int k, a[11];
        scanf("%d", &k);
        for (int i = 1; i <= k; ++i)
            scanf("%d", &a[i]);
        State s(k, a);
        printf("%d\n", search(k, s));
    }
    return 0;
}

康托展开+哈希

显然,我们可以将排列通过康托展开来映射为一个数,然后开一个哈希表来进行 \(O(1)\) 查找。

int calc(int n, int *arr) {
    int vis[20];
    memset(vis, 0, sizeof(vis));
    int res = 1;
    for (int i = 1; i < n; ++i) {
        int x = arr[i], cnt = 0;
        vis[x] = 1;
        for (int j = 1; j < x; ++j) if (!vis[j]) cnt++;
        //f[k]=k!
        res += cnt * f[n - i];
    }
    return res;
}

不过比较尴尬的是,我们构造出来的操作序列往往不止一种,很有可能是若干种(例如将排列 \([6,4,2,5,3,1]\) 变为 \([1,2,3,4,5,6]\),那么构造的操作序列应为 \([6,4,2,5,3,1,*,*,*,*]\),即后四位不固定)。那么,我们总不能一个个枚举来查吧?

康托展开有一个比较让人平和的性质:假设一个排列的前 \(m\) 位确定了,后 \(n-m\) 位不确定,那么记后 \(n-m\) 位从小到大排序,所得康托展开值为 \(f+1\),那么其从大到小排序所得的展开值为 \(f+(n-m)!\),且所有可能排列的区间值均在 \([f+1,f+(n-m)!]\) 之间。

我们可以开一棵线段树当哈希表,然后每次查询看看区间有没有符合要求的值即可。

//代码略,有空去参考下别的dalao写的代码
posted @ 2022-03-05 15:01  cyhforlight  阅读(121)  评论(0编辑  收藏  举报