CSP-S模拟19

T1.木棍

结论题。先列举所有可能的组合情况,容易发现可以先让\(3、3、4\)抵消,然后分情况讨论:1.\(3\)多,那么\(2、2、3、3\)消;2.\(4\)多,那么\(2、4、4\)消,如果最后还剩一个\(4\),那就\(2、2、2、4\),最后\(2、2、2、2、2\)把剩下的\(2\)消完。

代码
#define sandom signed
#include <bits/stdc++.h>
#define int long long 
using namespace std;
inline char getc() { static char buf[1 << 18], *p1, *p2; if (p1 == p2) { p1 = buf, p2 = buf + fread(buf, 1, 1 << 18, stdin); if (p1 == p2) return EOF; } return *p1++; }
inline int read() { int x = 0, f = 0; char c = getc(); while (!isdigit(c)) f = c == '-', c = getc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getc(); return f ? -x : x; }
inline void write(int x) { static int wrt[20]; int TP = 0; if (x < 0) putchar('-'), x = -x; while (x >= 10) wrt[++TP] = x % 10, x /= 10; wrt[++TP] = x; while (TP) putchar(wrt[TP--] | 48); putchar('\n'); }
inline int max(int a, int b) { return a > b ? a : b; } inline int min(int a, int b) { return a < b ? a : b; }

int a, b, c, k, ans;
sandom main()
{
    int T = read();
    while (T--)
    {
        a = read(), b = read(), c = read(); ans = 0;
        k = min(b >> 1, c); ans += k;
        b -= 2 * k, c -= k;//2个3、1个4
        if (!c)//还有3
        {
            k = min(a >> 1, b >> 1); ans += k;
            a -= 2 * k, b -= 2 * k;//2个2、2个3
        }
        else //还有4
        {
            k = min(a, c >> 1); ans += k;
            a -= k, c -= 2 * k;//1个2、2个4
        }
        if (c == 1 && a >= 3) ans++, c--, a -= 3;//3个2、1个4
        ans += a / 5;//5个2
        write(ans);
    }
    return 0;
}

T2.环

定义\(dp[i][j]\)为考虑到左边前\(i\)个位置,右边前\(a[i]\)个位置,右边的点构成\(j\)条有向路径(链)(把单点也看成一条有向路径)的方案数。为了更方便的转移,不妨先将\(a\)排序。考虑新加入第\(i\)个点,这时右边新加入\(a_i-a_{i-1}\)这些点,从这些点中选择若干个作为单点插入点集,得到转移

\[dp[i][j]+=\sum\limits_{k=0}^{a_i-a_{i-1}}C_{a_i-a_{i-1}}^{k}dp[i-1][j-k] \]

这时左侧第\(i\)个点是独立的,没有与任何点连边,那也就方便了构造环。从我们已经构造出的链中选取一条,并将这条链的起点和终点分别和\(i\)连边,构成一个环(因为这时起点和终点都在\(a_i\)前面),还要减去单点构成的路径,它们不构成回路。所以有

\[ans+=dp[i][1]-a[i] \]

这时我们选取两条链,让它们通过第\(i\)个点合并成一条链,因为有向,显然每一组都有两种合并方式,有转移

\[dp[i][j]+=2C_{j+1}^{2}dp[i][j+1] \]

正序枚举是为了保证合并之前没有与\(i\)连边。不难发现,单向路径的起点和终点都属于右侧点。

代码
#define sandom signed
#include <bits/stdc++.h>
#define re register int
#define int long long 
#define rep(i, a, b) for (re (i) = (a); (i) <= (b); ++(i))
#define dwn(i, a, b) for (re (i) = (a); (i) >= (b); --(i))
using namespace std;
const int Z = 5010; const int mod = 998244353;
inline char getc() { static char buf[1 << 18], *p1, *p2; if (p1 == p2) { p1 = buf, p2 = buf + fread(buf, 1, 1 << 18, stdin); if (p1 == p2) return EOF; } return *p1++; }
inline int read() { int x = 0, f = 0; char c = getc(); while (!isdigit(c)) f = c == '-', c = getc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getc(); return f ? -x : x; }
inline int max(int a, int b) { return a > b ? a : b; } inline int min(int a, int b) { return a < b ? a : b; }
inline int qpow(int a, int b, int c) { int res = 1; while (b) { if (b & 1) res = 1ll * res * a % c; a = 1ll * a * a % c; b >>= 1; } return res; }

int n, m, ans;
int a[Z], dp[Z][Z];
int fac[Z], inv[Z];
void init(int n)
{
    fac[0] = 1;
    rep(i, 1, n) fac[i] = fac[i - 1] * i % mod;
    inv[n] = qpow(fac[n], mod - 2, mod);
    dwn(i, n - 1, 0) inv[i] = inv[i + 1] * (i + 1) % mod;
}
inline int C(int n, int m) { return fac[n] * inv[m] % mod * inv[n - m] % mod; }

sandom main()
{
    n = read();
    init(n);
    rep(i, 1, n) a[i] = read();
    sort(a + 1, a + 1 + n);
    dp[0][0] = 1;
    rep(i, 1, n) 
    {
        int tmp = a[i] - a[i - 1];
        rep(j, 0, a[i]) dwn(k, min(tmp, j), 0) (dp[i][j] += C(tmp, k) * dp[i - 1][j - k]) %= mod;//选择新加入的点
        (ans += dp[i][1] - a[i]) %= mod;//强制通过第i个点构成环,并减去不构成回路的单点
        rep(j, 0, a[i]) (dp[i][j] += dp[i][j + 1] * C(j + 1, 2) * 2) %= mod;//通过i合并两条链
    }
    cout << ans * qpow(2, mod - 2, mod) % mod;
    return 0;
}

T3.传令

二分版的将军令。可以二分所用时间,然后贪心得到最少需要几个城市,与\(k\)进行比较。当二分时间为\(d\)时,一个显然的贪心:把节点从深到浅排序依次考虑,一个节点被覆盖到至少需要让它的\(d\)级祖先被选,但是如果路径上有其他可以满足的点,就直接跳出。可以\(bfs\);也可以加一个标记优化,然后直接跳父亲;最好把能覆盖的点提前删掉,就不需要跳父亲了。

代码
#define sandom signed
#include <bits/stdc++.h>
#define re register int
#define rep(i, a, b) for (re (i) = (a); (i) <= (b); ++(i))
#define ede(i, rt) for (re i = head[rt]; i; i = e[i].ne)
using namespace std;
const int Z = 2e5 + 10; 
inline char getc() { static char buf[1 << 18], *p1, *p2; if (p1 == p2) { p1 = buf, p2 = buf + fread(buf, 1, 1 << 18, stdin); if (p1 == p2) return EOF; } return *p1++; }
inline int read() { int x = 0, f = 0; char c = getc(); while (!isdigit(c)) f = c == '-', c = getc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getc(); return f ? -x : x; }
inline int max(int a, int b) { return a > b ? a : b; } inline int min(int a, int b) { return a < b ? a : b; }

int n, m, k, ans;
struct edge { int v, ne; } e[Z << 1];
int head[Z], cnt, p[Z];
inline void add(int x, int y) { e[++cnt] = edge{y, head[x]}; head[x] = cnt; }
int dep[Z], dad[Z], kid[Z];
bool del[Z];
inline bool cmp(int x, int y) { return dep[x] > dep[y]; }
void dfs(int rt, int fa)
{
    dep[rt] = dep[fa] + 1, dad[rt] = fa;
    ede(i, rt) if (e[i].v != fa) dfs(e[i].v, rt);
}
void update(int rt, int x, int d)
{
    rep(i, 0, d)
    {
        kid[rt] = min(kid[rt], dep[x]);
        if (dad[rt] && i != d) rt = dad[rt];
    }
}
int query(int rt, int x, int d)
{
    rep(i, 0, d) 
    {
        if (dep[x] + kid[rt] - 2 * dep[rt] <= d) return -rt;
        if (dad[rt] && i != d) rt = dad[rt];
    }
    return rt;
}
void clear(int rt)
{
    if (del[rt]) return;
    del[rt] = 1;
    ede(i, rt) if (e[i].v != dad[rt]) clear(e[i].v);
}
bool check(int d)
{
    int tot = 0;
    rep(i, 1, n) kid[i] = 1e9, del[i] = 0;
    rep(i, 1, n)
    {
        if (del[p[i]]) continue;//已经被删除了
        int j = query(p[i], p[i], d);
        if (j > 0) tot++, update(j, j, d);//需要一个新的接收点
        clear(abs(j));//清空子树
        if (tot > k) return false;//超过数量
    }
    return true;
}

sandom main()
{
    n = read(), k = read();
    rep(i, 2, n)
    {
        int x = read(), y = read();
        add(x, y), add(y, x);
    }
    dfs(1, 0);
    rep(i, 1, n) p[i] = i;
    sort(p + 1, p + 1 + n, cmp);
    int l = 1, r = n / k;
    while (l <= r)//最短的时间
    {
        int mid = l + r >> 1;
        if (check(mid)) ans = mid, r = mid - 1;
        else l = mid + 1;
    }
    cout << ans;
    return 0;
}

T4.序列

正难则反,考虑容斥。先计算出总方案数,不考虑\(n\)的序列是否是彩色序列,显然有

\[(n-m+1)k^{n-m} \]

表示枚举\(a\)出现的位置,剩下的位置随便放。
先求出一些方案数,定义\(dp[i][j]\)为长度为\(i\),末尾一段互不相同的数字最长为\(j\)的非彩色序列的排列数量。转移有两种情况:1.在末尾添加一个在\([i-j+1,i]\)中没有出现过的,那么\(j+1\),否则枚举新加入的数与前面哪一个数冲突了,\(j\)变为后面新的一段,手磨一下,发现:

\[dp[i][j]=(k-(j-1))dp[i-1][j-1]+\sum\limits_{t=j}^{k-1}dp[i-1][t] \]

可以用后缀和优化,定义\(sum[i][j]=\sum\limits_{t=j}^{k-1}dp[i][t]\),这个东西后面也有用。
\(a\)在非彩色序列中出现的次数,分三种情况考虑:
1.\(a\)本身就是彩色序列,那么\(n\)也一定是彩色序列,不合法的次数为\(0\),总方案数就是答案。
2.\(a\)中存在相等的数字,那么\(k\)序列一定不会包含\(a\),这时分别考虑左侧和右侧出现\(k\)序列,在原序列中找到左边最长一段互不相同的数长度为\(l\),右边为\(r\),那么有

\[\sum\limits_{i=0}^{n-m}\frac{sum[i+l][l]}{A_k^l}\frac{sum[n-(i+m)+r][r]}{A_k^r} \]

枚举\(a\)的位置,前面有\(i\)个空余的位置将它与\(a\)左侧的\(l\)连接,且不让它构成彩色序列,那么方案就是\(sum[i+l][l]\),只需要保证后缀最长的互不相同的长度小于\(k\)即可,除以排列数是因为\(l\)这一段已经确定;注意到我们的\(dp\)定义不仅可以是从前往后放,也可以从后往前放,所以也满足前缀和性质,因此右侧同理。因为相互独立,所以运用乘法原理。
3.\(a\)中不存在相同的数,那么\(a\)可能被\(k\)序列包含,这时枚举向左右分别还扩展多长。

\[\sum\limits_{i=m}^{n}\sum\limits_{j=m}^{min(k-1,i)}\frac{dp[i][j]}{A_k^m}\frac{sum[n-(i-j)][j]}{A_k^j} \]

枚举\(a\)的末尾位置,结尾最长扩展\(j\),那么\(n-(i-j)\)这后面一整段的最长扩展范围应是左侧基础的\(j\)和右侧的\([0,k-1-j]\)。前面的排列数是因为\(a\)整个已经确定,后面是因为对于\(dp[i][j]\)的每一种情况,\(j\)个位置已经确定。

代码
#define sandom signed
#include <bits/stdc++.h>
#define re register int
#define int long long
#define rep(i, a, b) for (re (i) = (a); (i) <= (b); ++(i))
#define _rep(i, a, b) for (re (i) = (a); (i) < (b); ++(i))
#define dwn(i, a, b) for (re (i) = (a); (i) >= (b); --(i))
using namespace std; 
const int Z = 25010; const int W = 410; const int mod = 1e9 + 7;
inline char getc() { static char buf[1 << 18], *p1, *p2; if (p1 == p2) { p1 = buf, p2 = buf + fread(buf, 1, 1 << 18, stdin); if (p1 == p2) return EOF; } return *p1++; }
inline int read() { int x = 0, f = 0; char c = getc(); while (!isdigit(c)) f = c == '-', c = getc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getc(); return f ? -x : x; }
inline int max(int a, int b) { return a > b ? a : b; } inline int min(int a, int b) { return a < b ? a : b; }
inline int qpow(int a, int b, int c) { int res = 1; while (b) { if (b & 1) res = 1ll * res * a % c; a = 1ll * a * a % c; b >>= 1; } return res; }

int n, m, k, ans;
int a[Z], cnt[Z];
int dp[Z][W], sum[Z][W];
int fac[Z], inv[Z];
void init(int n)
{
    fac[0] = 1;
    rep(i, 1, n) fac[i] = fac[i - 1] * i % mod;
    inv[n] = qpow(fac[n], mod - 2, mod);
    dwn(i, n - 1, 0) inv[i] = inv[i + 1] * (i + 1) % mod;
}
inline int _A(int n, int m) { return fac[n - m] * inv[n] % mod; }
bool judge()
{
    _rep(i, 1, k) cnt[a[i]]++;
    rep(i, k, m)
    {
        cnt[a[i - k]]--, cnt[a[i]]++;
        bool op = 1;
        rep(j, 1, k) if (cnt[j] > 1) { op = 0; break; }
        if (op) return true;//a是彩色序列
    } 
    return false;
}

sandom main()
{
    n = read(), k = read(), m = read();
    init(n);
    rep(i, 1, m) a[i] = read();
    ans = (n - m + 1) * qpow(k, n - m, mod) % mod;//总方案数
    if (judge()) { cout << ans; return 0; }
    dp[0][0] = sum[0][0] = 1;
    rep(i, 1, n) 
    {
        dwn(j, k - 1, 1) dp[i][j] = ((k - j + 1) * dp[i - 1][j - 1] % mod + sum[i - 1][j]) % mod;
        dwn(j, k - 1, 1) sum[i][j] = (sum[i][j + 1] + dp[i][j]) % mod;//后缀和
    }
    int l = 0, r = m + 1;//两端最长的互不相同
    memset(cnt, 0, sizeof(cnt));
    while (l < m && !cnt[a[l + 1]]) cnt[a[++l]] = 1;
    memset(cnt, 0, sizeof(cnt));
    while (r > 1 && !cnt[a[r - 1]]) cnt[a[--r]] = 1;
    r = m - r + 1;
    if (l < m)//存在相同的
    {
        rep(i, 0, n - m) 
            ans -= sum[i + l][l] * _A(k, l) % mod * sum[n - m - i + r][r] % mod * _A(k, r) % mod, ans %= mod;
    }
    else//互不相同的
    {
        rep(i, m, n) rep(j, m, min(k - 1, i))
            ans -= dp[i][j] * _A(k, m) % mod * sum[n - i + j][j] % mod * _A(k, j) % mod, ans %= mod;
    }
    cout << (ans % mod + mod) % mod;
    return 0;
}
posted @ 2022-10-17 07:45  sandom  阅读(60)  评论(5编辑  收藏  举报