Loading

12.28 CW 模拟赛 赛时记录

前言

还是只管考试的策略, 别想其他的

每个题控制用时, 根据时间选择策略, 冷静

看题

完蛋了是 \(\rm{NOIP}\) , 我们没救了

\(\rm{T1}\)

怎么办, 像是很典的题但是我多半做不出来
别人做过容斥的肯定会, 但是我就不一样了

\(\rm{T2}\)

好像也不会做

\(\rm{T3}\)

基环树上的 \(\rm{dp}\) ?

\(\rm{T4}\)

有点像一位故人出的题

\(\rm{T1}\)

思路

题意非常清楚, 就是用 \(S\) 中的数字在 \([A, B]\) 区间中拼出 \(X\) 的倍数

你发现 \(A, B, X\)\(10^{11}\) 数量级的, 很不好做

有倍数条件的同时还有数字的约束, 不太好做

你发现如果 \(S\) 是满的, 那么相当于问在 \([A, B]\) 区间中有多少个 \(X\) 的倍数, 这个是好求的

考虑引入 \(S\) 的限制, 朴素的想法是枚举倍数判断是否拥有其他数字, 这个显然不够优秀, 考虑有没有优化的方法

好吧我不会了, 打个暴力看下给了多少

代码

#include <bits/stdc++.h>
#define int long long

int X, A, B;
bool S[10];
int st; // 开始枚举的位置
int ans = 0;

bool check(int x) {
    while (x) {
        if (!S[x % 10]) return false; x /= 10; }
    return true;
}

signed main()
{
    scanf("%lld %lld %lld", &X, &A, &B);
    std::string a; std::cin >> a; for (int i = 0; i < a.length(); i++) S[a[i] - '0'] = true;
    st = (A % X) ? A / X + 1 : A / X; st *= X;

    for (int i = st; i <= B; i += X) {
        if (check(i)) ans++;
        if (clock() > 990) {
            printf("%lld", ans); return 0; }
    }
    printf("%lld", ans);

    return 0;
}

\(\rm{T2}\)

思路

这个题说不定比 \(\rm{T1}\) 可做?
好像明显有点畏难, 但是他一点提示没有不敢做

首先因为忘了欧氏距离是什么, 先玩一下样例

好的欧氏距离是, 对于两个点 \((x_1, y_1), (x_2, y_2)\) , 其欧氏距离 \(d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}\)

转化题意, 对于 \(n\) 个带权 \(k\) 的点, 任意两点 \(i, j\) 之间有双向连边, 其边权为 \(w_{i, j} = d_{i, j}\) , 求一最小阈值 \(C\) , 满足对于所有 \(w \leq C\) 的边连接后, 存在一个连通块 \(G\), 使得

\[\sum_{i = 1}^{\lvert G \rvert} (a_i \cdot k_i) \equiv 0 {\pmod K} , a_i \in \{0, 1\} \]

容易发现的是, 如果你要选择一些确定的点, 使其在同一连通块下, 那么你是可以求出最小阈值 \(C\) 的, 具体的, 我们可以二分之后 \(\rm{check}\) , 这个是 \(\mathcal{O} (n \log V)\) 的, 其中 \(V\) 是值域, 当然你也可以在确定 \(C\) 的情况下检查是否让老板开心

现在问题转化成了, 如何在一个图的连通块中, 判断是否可能有一些点的点权之和为 \(K\) 的倍数, 这个用可行性 \(\rm{dp}\) 可以解决

所以我们初步可以有一个暴力, 首先实数二分 \(V\) , 在这个基础上你把图的连通块跑出来 \(\mathcal{O} (n)\), 然后对于每个连通块进行可行性 \(\rm{dp}\)

好像还没说可行性 \(\rm{dp}\) 该怎么做

\(dp_{i, j}\) 表示考虑到第 \(i\) 个点权, 模 \(K\) 的结果是否能达到 \(j\) , 其中 \(dp_{i, j} \in \{0, 1\}\)

显然有转移

\[\begin{align*} dp_{i, j} \gets dp_{i - 1, j} \\ dp_{i, (j + k_i)} \gets dp_{i - 1, j} \end{align*} \]

其中 \(j\) 随时取模, 甚至可以滚掉 \(i\)


总结一下, 首先实数二分当前阈值 \(\mathcal{O} (\log V)\) , 然后跑当前图的情况和可行性 \(\rm{dp}\) \(\mathcal{O} (n\omega)\) , 其中 \(\omega = 30\) , 然后直接判断

总复杂度 \(\mathcal{O} (n \log V)\) , 可以通过本题

写的时候遇到了问题, 不能用 \(\mathcal{O} (n^2)\) 的方式建图, 我们需要在不计算原图的情况下找到连通块, 好像也不影响实现

实现

框架

实数二分, \(\rm{check}\) 比较复杂

代码

/*相信 O2 优化*/
#include <bits/stdc++.h>
// #define FILE_IO
#define int long long
const int MAXN = 5e4 + 20;
const double eps = 1e-4;
const int MAXVAL = 40;

#define dist(a, b) sqrt((double)(p[a].x - p[b].x) * (p[a].x - p[b].x) + (p[a].y - p[b].y) * (p[a].y - p[b].y))

int n, K;
struct node {
    int x, y, k;
} p[MAXN];

int be[MAXN], cnt;
/*辅助处理连通块的 bfs*/
void bfs(int now, int divnum, double C) {
    be[now] = divnum;
    for (int i = 1; i <= n; i++) {
        if (be[i] || dist(now, i) - C > eps) continue;
        bfs(i, divnum, C);
    }
}

std::vector<int> Div[MAXN];
/*在当前阈值情况下处理连通块*/
void divide(double C) {
    memset(Div, 0, sizeof(Div));
    for (int i = 1; i <= n; i++) be[i] = 0; cnt = 0;
    for (int i = 1; i <= n; i++) if (!be[i]) bfs(i, ++cnt, C);
    for (int i = 1; i <= n; i++) Div[be[i]].push_back(i);
}

/*检查阈值是否合法*/
bool check(double C) {
    divide(C);

    /*在当前连通块中处理可行性 dp*/
    for (int k = 1; k <= cnt; k++) {
        bool flag = false; // 是否可以整除
        int now = 0, nxt = 1;
        int dp[2][MAXVAL][2]; memset(dp, false, sizeof(dp)); dp[now][0][0] = true;
        for (int i = 0; i < Div[k].size(); i++) {
            std::swap(now, nxt); memset(dp[now], false, sizeof(dp[now]));
            int nowk = p[Div[k][i]].k;
            for (int j = 0; j < 40; j++) {
                dp[now][j][0] |= dp[nxt][j][0];
                dp[now][j][1] |= dp[nxt][j][1];
                dp[now][(j + nowk) % K][1] |= dp[nxt][j][0];
                dp[now][(j + nowk) % K][1] |= dp[nxt][j][1];
            }
            if (dp[now][0][1]) flag = true;
        }

        if (flag) return true;
    }
    return false;
}

/*二分答案*/
double binsearch() {
    double left = 0.0, right = 100000000.0;
    while (right - left > eps) {
        double mid = left + (right - left) / 2.0;
        if (check(mid)) right = mid;
        else left = mid;
    }
    return left;
}

signed main()
{
#ifdef FILE_IO
    freopen("connect_ex1.in", "r", stdin);
    freopen("connect_ex1.out", "w", stdout);
#endif

    scanf("%lld %lld", &n, &K);
    for (int i = 1; i <= n; i++) scanf("%lld %lld %lld", &p[i].x, &p[i].y, &p[i].k);
    // std::cout << check(1.3);

    double ans = binsearch();
    printf("%.3f", ans);

    return 0;
}

\(\rm{T3}\)

看起来很板的基环树上 \(\rm{dp}\) , 没时间做了

看起来也不能再写点什么了, 那就推推这个

套路的, 先考虑单棵树上怎么操作
\(f_{i, 0/1}\) 表示对于 \(i\) 的子树, 其中选不选当前点为关键点, 至少需要指定几个关键点才能符合要求

对于叶子节点的初始化是显然的, 考虑树形 \(\rm{dp}\) 的转移, 有

\[\begin{align*} f_{i, 0} \stackrel{k}{\longleftarrow} \left(\sum_{j = 1}^{\lvert son \rvert} f_{j, 0}\right) - f_{k, 0} + f_{k, 1} \\ f_{i, 1} \stackrel{k}{\longleftarrow} \left(\sum_{j = 1}^{\lvert son \rvert} f_{j, 0}\right) - f_{k, 0} + f_{k, 1} \end{align*} \]

显然的, 这样子会漏统计一种情况 : 子树的根节点本来是没有关键点与之相连的, 但是在 \(i\) 为关键点的时候子树成立

这个时候我们朴素的考虑往上加一维表示这个子树是否有关键点与之相连, 反正如果隔了一层一定不成立, 所以这样做是有正确性的

考虑令 \(f_{i, 0/1, 0/1}\) 表示对于 \(i\) 的子树, 其中选不选当前点为关键点, 当前子树的直接儿子中有没有关键点

\[\begin{align*} \end{align*} \]

posted @ 2024-12-28 15:57  Yorg  阅读(20)  评论(0)    收藏  举报