CSP-S 24

9.21

今天开始集训告一段落了,去补文化课一周。

如果写不完回来会补(?)

115=100+0+15

t1

先等会

t2

先等会

t3

9.22:回来补债了

\([\gcd(i,j)=1]=[\gcd(p_i,p_j)=1]\)

这个限制初看好像很难转化,只能猜到与组合数和素数有关。

于是我们从原始情况入手,考虑如何变换之后得到的排列是符合条件的。

发现对于 2,4,8,16 \(\cdots\) 这些数任意交换位置后得到排列都是合法的,考虑从此入手。

进一步手模数据,发现关系一:

对于所有质因数集合相同的数,任意交换位置后排列合法

解释:我们只关心两个数及其下标是否互质,因此我们只关心其质因数集合是否相同。(多模会数据就理解了)

可是这还不够,继续寻找。

关系二:

\(1\le i \le n\) 中,对于所有满足 $\lfloor \frac{n}{pri_i} \rfloor\le 1 $ 的素数可以与 \(1\) 交换位置

解释:\(1\) 永远与所有数互质,而对于 $\lfloor \frac{n}{pri_i} \rfloor\le 1 $ 的素数在 \(1\le i \le n\) 范围内也与所有数互质,与 \(1\) 等效,故交换后合法。

此时已经可以过掉 \(n=13\) 了,但当 \(n=14\) 时答案依旧错误。

发现错误原因:5,10 与 7,14 可以整体交换!

我们发现它们的质因数的出现次数相同 ,即说明对于一个质因数的约束,我们只关心其约束条件是否成立而并不关心质因数的值,因此对于质因数出现次数相同也可整体交换

此为关系三。

到此结论已推完,实现即可。

交换对应排列,乘对应阶乘即可。

只需找出所有质因数集合并统计个数 \(\cdots\)

等等,这一步咋实现 ?

map 里放 vector

这个我也是赛后才知道可以这么套,学到了。

具体实现看代码。

code

点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 10;
const int p = 13131;
const int mod = 1e9 + 7;
int n, ans;
int f[N], cnt[N];
int pri[N], tot;
int t[N];
bool flag[N];
vector<int> e[N];
map<vector<int>, int> mp;

inline void init()
{
    f[0] = 1, f[1] = 1;
    flag[1] = 1;
    for (int i = 2; i <= n; ++i)
    {
        f[i] = f[i - 1] * i % mod;
        if (!flag[i])
            pri[++tot] = i;
        for (int j = 1; j <= tot; ++j)
        {
            if (i * pri[j] > n)
                break;
            flag[i * pri[j]] = 1;
            if (i % pri[j] == 0)
                break;
        }
    }
}

signed main()
{
    freopen("tg.in", "r", stdin);
    freopen("tg.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n;
    init();

    for (int j = 1; j <= tot; ++j)
        for (int i = pri[j]; i <= n; i += pri[j])
        {
            e[i].push_back(j);
            ++cnt[j];
        }

    for (int i = 1; i <= n; ++i)
    {
        if (!flag[i])
        {
            if (e[i].size() == 1 && cnt[e[i][0]] == 1)
                e[i][0] = tot + 1;//将关系二中满足条件的素数与1放入一个集合
        }
        if (i == 1)
            e[i].push_back(tot + 1);
        ++mp[e[i]];
    }

    ans = 1;

    for (int i = 1; i <= tot; ++i)
        ++t[cnt[i]];

    for (int i = 2; i <= n; ++i) // 5 10 7 14 可互换:质因数出现次数相同时可互换,桶记录
        ans = ans * f[t[i]] % mod;

    for (auto y : mp) // 记录质因数种类相同的数的个数
        ans = ans * f[y.second] % mod;

    cout << ans << "\n";
    return 0;
}

t4

9.23:补ing

看完题第一感觉就是最小生成树,但发现将所有边连出再生成显然要爆炸,而只对每个城市内的图做最小生成树又不好处理高铁的情况。

这时我们要开始利用最小生成树的思想。

肯定要用到最小生成树,再做一部分后续处理。

对于每个城市内的图是同构的,显然可以做一遍最小生成树,先筛选出可能被选的边,再进行后续处理。

我们把枢纽站叫作关键点。

若无高铁站的情况,则显然为每个城市内最小生成树。

考虑有高铁站后的影响:

对于城市内的点,不一定可以在城市内直接到达,也就是说,若建造高铁站更优,则可以通过高铁站到达其它城市,再到达目标点,即在同一城市内,图不一定连通

考虑如何选择这些边:

观察易得,所有不在关键点之间路径上的树边都一定会被选,为了保证整张图连通,这些边必选,直接累加答案即可。

对于其它边,将其记录下来( \(e\) 数组)当作备选。

高铁站更优当且仅当 \(a_i\le b_i+d_j\) ,即 \(a_i-b_i\le d_j\)

也就是说,对于一个城市,所有边权(被选出的边)大于 \(a_i-b_i\) 的边,都应不修建。

这启发我们对于城市按 \(a_i\) 升序排列(后有解释),对于 \(e\) 数组按照边权升序进行排序,求前缀和,随后对于每个城市,二分找出哪一条边以后都应不修建,利用前缀和求出应修建的边的贡献,不修建的边数乘 \(a_i\) 作为修高铁的贡献。

对于 \(1\) 号城市与 \(l\) 号城市直接连权值最小的前 \(r-1\) 条边保证在最后一个城市内部连通,否则若没有城市内部连通,则高铁再多图也不连通。(这时体现将城市排序的作用,修建高铁时选择费用少的,对于费用最大的城市令其内部连通,以此在保证图连通时总费用最小)。

关于如何选出关键点之间的路径:

我们将关键点的 \(fa\) 都设为 \(0\) ,表示它们已被连通,之后正常最小生成树,若 \(find(u)\not=find(v)\) ,则不为关键点路径上的边,直接累加答案即可,否则表示为关键点路径上的点(因为所有关键点已事先“被连通”,发现两点的 \(fa\) 相同则一定为关键点加入树中路径上的点,不理解就造样例手模),将其加入 \(e\) 数组即可。

code

点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int n, m, l, r, sumb, ans;
int fa[N], id[N];
int a[N], b[N], sumq[N];
int s[N], q[N], tot;
struct node
{
    int u, v;
    int val;
} e[N];
inline int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }
inline bool cmp(node a, node b) { return a.val < b.val; }
inline bool cmp2(int x, int y) { return a[x] < a[y]; }

signed main()
{
    freopen("rb.in", "r", stdin);
    freopen("rb.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= m; ++i)
    {
        cin >> e[i].u >> e[i].v >> e[i].val;
        ++e[i].u, ++e[i].v;
    }
    sort(e + 1, e + 1 + m, cmp);
    for (int i = 1; i <= n; ++i)
        fa[i] = i;
    for (int i = 1; i <= m; ++i) // 先最小生成树降低边数至n
    {
        int fu = find(e[i].u), fv = find(e[i].v);
        if (fu != fv)
            e[++tot] = e[i], fa[fu] = fv;
    }

    for (int i = 1; i <= n; ++i)
        fa[i] = i;
    cin >> l;
    for (int i = 1; i <= l; ++i)
        cin >> a[i] >> b[i], sumb += b[i];
    cin >> r;
    for (int i = 1; i <= r; ++i)
        cin >> s[i], ++s[i], fa[s[i]] = 0;
    tot = 0;
    for (int i = 1; i < n; ++i)
    {
        int u = find(e[i].u), v = find(e[i].v);
        if (u != v)
            fa[u] = v, ans += sumb + e[i].val * l;
        else
            q[++tot] = e[i].val;
    }

    for (int i = 1; i < r; ++i)
        sumq[i] = sumq[i - 1] + q[i];
    for (int i = 1; i <= l; ++i) // 城市之间
        fa[i] = id[i] = i;
    sort(id + 1, id + 1 + l, cmp2);
    for (int i = 1; i < l; ++i) // 连高铁
    {
        int x = id[i];
        int y = x % l + 1;
        x = find(x), y = find(y);
        if (b[x] < b[y])
            swap(x, y);
        int pos = upper_bound(q + 1, q + r, a[id[i]] - b[x]) - q - 1; // a b+d --> a-b d 二分找位置
        ans += b[x] * pos + sumq[pos] + (r - pos) * a[id[i]];
        fa[x] = y;
    }
    ans += b[find(1)] * (r - 1) + sumq[r - 1]; // a最大的直接连权值最小r-1条边保证连通
    cout << ans << "\n";
    return 0;
}

咕...

posted @ 2025-10-20 21:45  HS_fu3  阅读(7)  评论(0)    收藏  举报