link: 牛客

vp time: 250502 12:00-17:00

team: std::future

members: LittleYoo, Broken_Eclipse, f2021ljh

accepted: LEIKBCG

penalty: 1193

ranking: 26/238 (10.92%)

problem difficulty: LEIKBGCDFJAMH

L. Polygon(签到题)

给定 条边的长度,判断能否构成一个简单多边形。

多边形不等式:每条边小于其他所有边之和。

故判断最大的边是否满足即可。

E. Plus(结论题)

给定 ,求所有 满足 均为素数,且 为素数。

如果 均为奇数或均为 ,则 一定为偶数,故不为素数。

因此,满足条件的 只有 且为素数,且 为素数。

进一步发现,,又因为 为奇素数,若 则有 ,故 ,因此

综上, 为唯一的解。

I. Generator(期望 DP,Euler 常数)

给定一个正整数 ),每次操作在 内均匀随机生成一个整数,并将 变成这个整数。

求将 变成 的期望操作次数。误差不超过 即可。

为将 变成 的期望操作次数。当 时,有

移项得

同理有

相减得

根据题意,初值 。故

时,可 计算。

时,可以证明 的差是收敛的。

证明(梅加强):由 ,且数列 递增, 递减,且 收敛于 ,有不等式

两边取对数得

对于 ,将上述不等式相加,得

,则由右端不等式知 ,且有

故由单调有界准则知, 收敛,其极限记为 ,称为 Euler 常数,计算表明

可打表计算 的近似值,然后用 近似。

即可。

#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
typedef double db;
int const K = 1e7;
db const _gamma = 0.577215665402;
int n;
db ans = 1;

signed main() {

    cin >> n;
    if (n <= K) {
        f(i, 1, n - 1) ans += 1.0 / i;
    } else {
        ans += _gamma + log(n - 1);
    }
    cout << fixed << setprecision(10) << ans << '\n';

    // int const N = 1e9;
    // db sum = 0;
    // f(i, 1, N) sum += 1.0 / i;
    // db err = sum - log(N);
    // cout << fixed << setprecision(12) << err << '\n';

    return 0;
}

由于要求的精度不高,在 很大的情况下,可以将 近似为 ,这样复杂度是 ,取 即可通过。

#include <cstdio>
#include <algorithm>
#define ll long long
#define Reg register
using namespace std;
const int maxn = 210000;
int n;
double ans;
int main() {
    scanf("%d", &n);
    if (n == 1) ans = 1;
    else if (n == 2) ans = 2;
    else {
        ans = 2;
        int up = n - 1;
        double a = 0, b = 0;
        if (up <= 100000000 - 1) {
            for (Reg int i = 2; i <= up; ++i) ans += 1.0 / i;
        }
        else {
            int maxx = 100000000 - 1;
            for (Reg int i = 2; i <= maxx; ++i) ans += 1.0 / i;
            for (Reg int i = maxx + 1; i <= up; i += 4) ans += 4.0 / i;
        }
    }
    printf("%.10lf\n", ans);

    return 0;
}

由于 ,也可采用分段打表的方式。(我不会)

K. Maze(BFS)

给定一个 的网格,有一些网格是障碍。现在要从 走到 ,要求:

  • 每次可以向上下左右走一步;
  • 不能经过障碍;
  • 沿同一方向连续走不超过 步。

求最少步数。如果不能到达,输出

数据组数 。保证 的数据组数不超过

记状态 为当前在 网格,且在 方向上已经连续移动了 次,其中 ,分别表示上下左右。

为该状态下的最少步数,直接进行 BFS 即可,松弛操作类似最短路。

#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const INF = 0x3f3f3f3f;
int const N = 110;
int const dx[4] = {-1, 1, 0, 0};
int const dy[4] = {0, 0, -1, 1};
int tt, n, m, d[N][N][N][4];
char g[N][N];

struct Node {
    int x, y, k, dir;
};

void bfs() {
    memset(d, 0x3f, sizeof d);
    queue<Node> q;
    q.push({1, 1, 0, 0});
    d[1][1][0][0] = 0;
    while (!q.empty()) {
        auto [x, y, k, dir] = q.front();
        q.pop();
        f(i, 0, 3) {
            if (i == dir && k == m) continue;
            int xx = x + dx[i], yy = y + dy[i];
            if (xx < 1 || xx > n || yy < 1 || yy > n || g[xx - 1][yy - 1] == '*') continue;
            int nxk = (i == dir ? k + 1 : 1);
            int cur = d[x][y][k][dir], &nxt = d[xx][yy][nxk][i];
            if (cur + 1 < nxt) {
                nxt = cur + 1;
                q.push({xx, yy, nxk, i});
            }
        }
    }
    int ans = INF;
    f(i, 0, 4) f(j, 0, m) ans = min(ans, d[n][n][j][i]);
    if (ans == INF) cout << "-1\n";
    else cout << ans << '\n';
    return;
}

void solve() {
    cin >> n >> m;
    f(i, 0, n - 1) cin >> g[i];
    bfs();
    return;
}

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    cin >> tt;
    while (tt--) solve();

    return 0;
}

B. Capital Program(树的中心,贪心)

给定一棵 个节点的树,边权为

要求在树中选定一个大小为 的连通块,使得连通块外所有点到连通块的距离的最大值最小。

输出这个最小值。

如果 ,则一定要选树的中心(直径的任意一个中点)作为首都。

以中心为根,向下选 个点,根据贪心的思想,不断选与当前首都有连边的点中,子树深度最深的那个。

因此求出中心,以中心为根进行 DFS,求出每个节点的子树深度。

可以发现,选定的节点的子树深度一定是单调不增的。因此直接将子树深度按降序排序,那么第 个即为最后一个选择的节点,故为其他节点到连通块的最远距离。

时间

如何求树的中心?

下面用类似拓扑排序的方法找到树的中心,即:开始时将叶子节点放进队列,从树中删去队首并出队的同时,向队列中不断加入新的叶子,最后一个入队的即为中心。

#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define pb push_back
using namespace std;
typedef pair<int, int> pii;
int const N = 1e5 + 10;
int n, k;
vector<int> g[N];

bitset<N> used;
int deg[N];
int getCenter() {
    queue<int> q;
    f(i, 1, n) {
        if ((deg[i] = g[i].size()) == 1) {
            q.push(i);
            used[i] = 1;
        }
    }
    int u;
    while (!q.empty()) {
        u = q.front();
        q.pop();
        for (int v: g[u]) {
            if (!used[v] && (--deg[v] == 1)) {
                q.push(v);
                used[v] = 1;
            }
        }
    }
    return u;
}

int dep[N];
void dfs2(int u, int fa) {
    for (int v: g[u]) if (v ^ fa) {
        dfs2(v, u);
        dep[u] = max(dep[u], dep[v]);
    }
    ++dep[u];
    return;
}

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    cin >> n >> k;
    f(i, 2, n) {
        int u, v;
        cin >> u >> v;
        g[u].pb(v), g[v].pb(u);
    }

    int cen = getCenter();
    dfs2(cen, 0);

    sort(dep + 1, dep + n + 1, greater<int>());
    cout << dep[k + 1] << '\n';

    return 0;
}

或者可以用换根 DP 的方式求中心,即求到其他点的最大距离最小的点。

int dup[N], ddn[N], ddn2[N], msn[N], msn2[N];
void dfs(int u, int fa) {
    for (int v: g[u]) if (v ^ fa) {
        dfs(v, u);
        if (ddn[v] + 1 >= ddn[u]) {
            ddn2[u] = ddn[u], msn2[u] = msn[u];
            ddn[u] = ddn[v] + 1, msn[u] = v;
        } else if (ddn[v] + 1 > ddn2[u]) {
            ddn2[u] = ddn[v] + 1, msn2[u] = v;
        }
    }
    return;
}

void dfs1(int u, int fa) {
    for (int v: g[u]) if (v ^ fa) {
        if (msn[u] == v) dup[v] = 1 + max(dup[u], ddn2[u]);
        else dup[v] = 1 + max(dup[u], ddn[u]);
        dfs1(v, u);
    }
    return;
}

int getCenter() {
    dfs(1, 0);
    dfs1(1, 0);
    int res = 1, mn = ddn[1];
    f(i, 2, n) {
        int cur = max(ddn[i], dup[i]);
        if (cur < mn) {
            mn = cur, res = i;
        }
    }
    return res;
}

C. Segment Tree(思维题;贪心;DP)

下面用 代替算法伪代码中的

对于长度为 的正整数序列 ,其中 ,执行上述算法,将序列插入动态开点线段树。

问如何构造序列 ,使得节点数 取到最大值?输出这个最大值。

数据组数

一棵线段树的形状一定是一个深度为 的满二叉树,在其基础上选择 个叶子,延伸出一层的左右儿子。

为了使选择的路径中包含尽可能多的未创建过的节点,应该左、右子树交替选择。

……(推出一个神必式子)

答案为

G. Hot Water Pipe(队列,暴力)

有一根热水管,由 节组成,编号为 ,每节容量为 单位体积。

节水管内的热水的初始温度为 ,且每分钟会下降 单位温度。

任一节水管中的热水温度一旦小于 ,就会瞬间被加热至 ,加热时间可忽略不计。

使用热水时,从第 节水管中取出 单位体积热水,同时前 节水管中的热水向后平移一节(温度不变),并在第一节水管中注入温度为 的热水。

次取水操作,每次操作在上次操作的第 分钟后,且使用了 单位体积的热水。

输出每次取出的热水温度的总和。

注意到,热水的温度是周期性变化的,所以如果知道初始温度和经过的时间,就可以知道最终的温度。

另外,每次取水操作中,新加入的 体积水的温度,无论何时总是相同的。

取水操作与加水操作的规则,符合队列的性质,故用队列来维护,元素为加水的体积和时间。

可以发现,在若干次取水操作之后,再取出的水,一定是后加入的水,而任意后加入的水的加入时间都是已知的,从而可以求出答案。

因此直接暴力模拟取水和加水的过程。

队列中同时最多有 个元素,每次弹出至多 个元素,加入 个元素,由势能分析知,时间复杂度是 的。

#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
#define fi first
#define se second
#define mp make_pair
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
int const N = 1e6 + 10;
int n, m, tmin, tmax, dt, a[N];
queue<pii> q;

int get_t(ll tim, int t0) {
    int t = t0 - tim % dt;
    return t < tmin ? t + dt : t;
}

int get_t0(ll tim) {
    return tmin - 1 + tim % dt;
}

signed main() {
    cin.tie(0)->sync_with_stdio(false);

    cin >> n >> m >> tmin >> tmax;
    dt = tmax - tmin + 1;
    f(i, 1, n) cin >> a[i];
    g(i, n, 1) q.push(mp(a[i], 1));
    ll tim = 0;
    while (m--) {
        int t, k;
        ll tot = 0;
        cin >> t >> k;
        int kk = k;
        tim += t;
        while (!q.empty() && q.front().se <= k) {
            k -= q.front().se;
            tot += get_t(tim, q.front().fi) * 1ll * q.front().se;
            q.pop();
        }
        if (q.empty() && k) {
            tot += 1ll * k * tmax;
        } else if (q.front().se > k) {
            q.front().se -= k;
            tot += get_t(tim, q.front().fi) * 1ll * k;
        }
        q.push(mp(get_t0(tim), min(kk, n)));
        cout << tot << '\n';
    }

    return 0;
}

*D. Game(博弈,结论)

堆石子,第 堆石子有 颗。Alice 和 Bob 轮流执行以下操作:

  • 选择一堆非空的石子。
  • 将若干颗石子从这堆中拿出,可以拿走全部,但不能不拿。
  • 对这堆中剩余的每个石子,可以选择不动,也可以选择放到另一堆非空的石子中。

次询问,每次询问给定 ,问用 进行游戏,谁有必胜策略?

下面证明:如果所有石堆的石子数的出现次数都为偶数,则先手必败,否则先手必胜。

  • 如果所有石堆的石子数的出现次数都为偶数,那么将所有石堆分成完全相同的两组,后手可以选择在另一组跟随先手的一切操作,最后清空的一定是后手,故后手必胜。

  • 如果存在一堆石子,石子数的出现次数为奇数:不妨设

    • ,则将 取走,同时对于所有 ,在 上放 个石子,由于

      这样做是可行的,得到的序列为 ,故出现次数均为偶数。

    • ,则从 中取走 个石子,同时对于所有 ,在 上放 个石子,类似地,可以证明,这样做是可行的,得到的序列为 ,故出现次数均为偶数。

对于静态询问区间内每个数的出现次数,可以用莫队实现。时间复杂度

然而我们不需要求出每个数的具体出现次数,只需要求是否有数字出现过奇数次。因此可以用 异或哈希 的 trick 来做:给每个数随机赋一个 64 位权值,求异或和,如果为零,则所有数出现的次数都是偶数,否则存在至少一个数出现了奇数次。