杂题合集 I

不是自己想出来的题打了星号。

* P13525 [KOI 2025 #2] 新的情缘

容斥好题,感觉思路很像 NOIP2024 T3。想的时候被 Sub3 的错排做法误导了一直没想到容斥。

区间不交的性质是很好的,这使得我们可以将区间包含的关系利用树形结构刻画。于是可以先建树,即如果 \(u\) 包含了 \(v\),那么连边 \(u\to v\)

首先有一个结论:不同弱连通块的点不能互相选。证明可以考虑最右边的弱连通块,如果左边的男的选了右边的一个女的,那么右边必然存在一个男的找不到女朋友。

因此最后的答案是每个弱连通块的乘积。

下文中,我们规定配对是指给左端点(男)找右端点(女)。

考虑弱化限制,如果两个人能复合时该怎么做。手模样例后能够发现一个男的能配对的女的个数一定是这个节点的深度。于是此时的答案是 \(\prod_{u\in T} \text{dep}_u\)

加入了不能复合的限制后,考虑容斥原理。

假设我们当前钦定复合的点集为 \(S\),那么其容斥系数为 \((-1)^{|S|}\)。如何计算其方案数呢?参考之前弱化限制的公式,可以想到答案是 \(\prod_{u\in T} a_u\)。其中 \(a_u\) 表示 \(\text{root}\to u\) 的路径上不在 \(S\) 中的点的个数。如果 \(u\)\(S\) 中则将 \(a_u\) 赋值为 \(-1\)

容易发现此时一种方案的容斥结果就是 \(\prod_{u\in T}a_u\)。我们考虑设计树形 DP 来维护这个式子。

因为一个节点 \(a_u\) 的值与其到根链在 \(S\) 中的个数有关,于是可以设计 DP:\(dp_{u, i}\) 表示 \(\text{root}\to u\) 的路径上(不包含 \(u\) 自己)有 \(i - 1\) 个点不在 \(S\) 里的情况下,\(\prod_{v\in \text{Subtree}_u}a_v\) 的值。转移可以进行分类讨论:

  • \(u\)\(S\) 里,则 \(dp_{u, i}\xleftarrow{+} -1\times \prod_{v \in \text{son}_u}dp_{v, i}\)
  • \(u\) 不在 \(S\) 里,则 \(dp_{u, i}\xleftarrow{+} (i - 1 + 1)\times \prod_{v \in \text{son}_u}dp_{v, i+1}\)。其中 \((i - 1 + 1)\) 的原因是 \(u\) 依然能和自己配对。

转移即可。时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 6005;
const ll mod = 1e9 + 7;
int n, rd[N], dep[N];
ll dp[N][N], f[N];
pi seg[N];
vector<int> g[N];
void dfs(int u)
{
    if(g[u].size() == 0)
    {
        for(int i = 1; i <= dep[u]; i++) dp[u][i] = i - 1;
        return;
    }
    for(auto v : g[u])
    {
        dep[v] = dep[u] + 1;
        dfs(v);
    }
    for(int i = 1; i <= dep[u] + 1; i++) f[i] = 1;
    for(auto v : g[u])
        for(int i = 1; i <= dep[v]; i++)
            f[i] = (f[i] * dp[v][i]) % mod;
    for(int i = 1; i <= dep[u]; i++)
        dp[u][i] = ((f[i + 1] * i % mod - f[i]) % mod + mod) % mod;
}
void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> seg[i].fi >> seg[i].se;
        g[i].clear();
        rd[i] = 0;
    }
    if(n == 1)
    {
        cout << "0\n";
        return;
    }
    sort(seg + 1, seg + n + 1);
    for(int i = 1; i <= n; i++)
    {
        int nowl = i;
        for(int j = i + 1; j <= n; j++)
        {
            if(seg[j].se > seg[i].se) break;
            if(seg[j].fi > nowl)
            {
                g[i].push_back(j);
                rd[j]++;
                nowl = seg[j].se;
            }
        }
    }
    ll ans = 1;
    for(int i = 1; i <= n; i++)
    {
        if(rd[i]) continue;
        dep[i] = 1;
        dfs(i);
        ans = (ans * dp[i][1]) % mod;
    }
    cout << ans << "\n";
}
int main()
{
    //freopen("sample.in", "r", stdin);
    //freopen("sample.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--) solve();
    return 0;
}

P8252 [NOI Online 2022 提高组] 讨论

先考虑如何判断无解。显然我们把有交集的人进行连边,如果形成的是一个森林则一定无解。

具体实现上,可以用并查集维护连通块。然后对于每个连通块里的人,按照会的题的数目从大到小排序后,让每个人检查自己会的题上的标记是否都是同一个人的,最后让他给自己会的题打上标记(之前的标记会被覆盖掉)。

一旦一个人检查到不合法,那么必然存在另一个人会和他进行讨论。证明可以进行分类讨论,比较繁琐此处略去。

于是我们暴力 check 每一个人,找到其同伙即可。

时间复杂度 \(O(n\log n + m)\),如果使用桶排序可以做到线性。

#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 2000005;
int n, tot[N], vis[N];
vector<int> g[N], gt[N];
struct DSU{
    int fa[N];
    void init()
    {
        for(int i = 1; i <= 2 * n; i++) fa[i] = i;
    }
    int findf(int x)
    {
        if(fa[x] != x) fa[x] = findf(fa[x]);
        return fa[x];
    }
    void combine(int x, int y)
    {
        int fx = findf(x), fy = findf(y);
        fa[fx] = fy;
    }
} dsu;
bool cmp(int x, int y)
{
    return (tot[x] > tot[y]);
}
void brute(int u)
{
    cout << "YES\n";
    memset(vis, 0, sizeof(vis));
    for(auto v : g[u]) vis[v] = 1;
    int anc = dsu.findf(u);
    for(auto us : gt[anc])
    {
        if(us == u) continue;
        bool legal1 = 0, legal2 = 0, legal3 = 0;
        int nowtot = 0;
        for(auto v : g[us])
        {
            if(vis[v] == 0) legal1 = 1;
            if(vis[v] == 1) legal2 = 1, nowtot++;
        }
        legal3 = (nowtot != tot[u]);
        if(legal1 && legal2 && legal3)
        {
            cout << u << " " << us << "\n";
            return;
        }
    }
}
void solve()
{
    cin >> n;
    dsu.init();
    for(int i = 1; i <= 2 * n; i++)
    {
        g[i].clear();
        gt[i].clear();
        vis[i] = 0;
    }
    for(int i = 1; i <= n; i++)
    {
        cin >> tot[i];
        for(int j = 1; j <= tot[i]; j++)
        {
            int x;
            cin >> x;
            g[i].push_back(x);
            dsu.combine(i, n + x);
        }
    }
    for(int i = 1; i <= n; i++)
    {
        int anc = dsu.findf(i);
        gt[anc].push_back(i);
    }
    for(int i = 1; i <= 2 * n; i++)
    {
        if(gt[i].size() == 0) continue;
        sort(gt[i].begin(), gt[i].end(), cmp);
        int mxu = gt[i][0], tid = 1;
        for(auto v : g[mxu])
            vis[v] = 1;
        for(auto u : gt[i])
        {
            if(u == mxu) continue;
            ++tid;
            int lst = -1;
            for(auto v : g[u])
            {
                if(lst == -1) lst = vis[v];
                if(vis[v] != lst)
                {
                    brute(u);
                    return;
                }
                vis[v] = tid;
            }
        }
    }    
    cout << "NO\n";
}
int main()
{
    //freopen("sample.in", "r", stdin);
    //freopen("sample.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--) solve();
    return 0;
}

* P8256 [NOI Online 2022 入门组] 字符串

这题的题解都在叽里咕噜说什么呢,感觉没有提到倒序操作的都没什么道理啊。

正做着是不好考虑的,因为不仅会删 \(R\) 的头还会删 \(R\) 的尾,根本无法快速求出当前 \(R\)\(T\) 匹配的部分。

我们考虑倒着做,初始时 \(R=T\)。那么操作就会变为:

  • \(S_i=\texttt{-}\) 时,在 \(R\) 前面或者后面加一个通配符 \(\texttt{*}\)。因为这个字符是被删掉的,所以它是什么都不会影响答案,起到的只是一个占位的作用。
  • \(S_i=\texttt{0/1}\) 时,必须要保证 \(R\) 的末尾能匹配上 \(S_i\),因为此时 \(R\) 的末尾会被删除。

在这个过程中我们不难发现:倒着操作的过程中,\(R\) 匹配上 \(T\) 的部分一定是一段 \(T\) 的前缀。因为我们无法在前面删数,只能在后面删。

于是可以定义状态 \(dp_{i, j, k, l}\) 表示从后往前考虑,已经执行了第 \(i\) 次操作,前面的通配符有 \(j\) 个,中间匹配上 \(T\) 的有 \(k\) 个,后面的通配符有 \(l\) 个的方案数。但是注意到 \(l = L - j - k\),所以可以将 \(l\) 这一维度省略。其中 \(L\) 表示当前时间下字符串应有的长度。

转移即可。时间复杂度 \(O(tmn^2)\)

#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 405;
const int mod = 1e9 + 7;
int n, m, L[N];
char s[N], t[N];
int dp[N][N][N];
void add(int &x, int y)
{
    x += y;
    if(x >= mod) x -= mod;
}
void solve()
{
    cin >> n >> m >> s + 1 >> t + 1;
    for(int i = 1; i <= n; i++)
    {
        if(s[i] == '-') L[i] = L[i - 1] - 1;
        else L[i] = L[i - 1] + 1;
    }
    memset(dp, 0, sizeof(dp));
    dp[n][0][m] = 1;
    for(int i = n; i >= 1; i--)
    {
        for(int j = 0; j <= n; j++)
        {
            for(int k = 0; k <= n; k++)
            {
                int l = L[i] - j - k;
                if(l < 0 || l > n) continue;
                if(s[i] == '-')
                {
                    add(dp[i - 1][j + 1][k], dp[i][j][k]);
                    add(dp[i - 1][j][k], dp[i][j][k]);
                }
                else
                {
                    if(l == 0 && k > 0 && t[k] != s[i]) continue;
                    if(l > 0) add(dp[i - 1][j][k], dp[i][j][k]);
                    else if(k > 0) add(dp[i - 1][j][k - 1], dp[i][j][k]);
                    else add(dp[i - 1][j - 1][k], dp[i][j][k]);
                }
            }
        }
    }
    cout << dp[0][0][0] << "\n";
}
int main()
{
    //freopen("sample.in", "r", stdin);
    //freopen("sample.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    cin >> t;
    while(t--) solve();    
    return 0;
}

P6570 [NOI Online #3 提高组] 优秀子序列

\(\varphi\) 和位运算显然没啥优美的性质,所以我们只能考虑对每个 \(\varphi\) 求出其贡献系数,即组成它的方案数。

于是考虑状压 DP:\(dp_{S}\) 表示当前和为 \(S\) 的方案数。转移可以考虑枚举子集:

\[dp_{S}\xleftarrow[S \oplus T > T]{+} dp_{T}\times f_{S \oplus T} \]

其中 \(f_T\) 表示初始时有多少个 \(T\)。转移时要保证 \(S \oplus T > T\) 的原因是,需要强制要求新加进来的一个数是最大的,否则把数字加进来的顺序也会影响答案。

最后需要特判 \(0\),因为可以放任意多个 \(0\) 到子序列里。

时间复杂度是经典的子集和复杂度 \(O(3^{\log V})\)。使用 FWT 可以做到更优的复杂度,但是我不会。

#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 1000005, V = 300005;
const ll mod = 1e9 + 7;
ll n, dp[V], a[N], ans, phi[N], cnt, prm[N], f[V];
bitset<N> vis;
ll qpow(ll a, ll b)
{
    ll res = 1;
    while(b)
    {
        if(b & 1) res = (res * a) % mod;
        b >>= 1;
        a = (a * a) % mod;
    }
    return res;
}
void init()
{
    phi[1] = 1;
    for(int i = 2; i < N; i++)
    {
        if(!vis[i])
        {
            prm[++cnt] = i;
            phi[i] = i - 1;
        }
        for(int j = 1; j <= cnt && i * prm[j] < N; j++)
        {
            int v = i * prm[j];
            vis[v] = 1;
            if(i % prm[j] == 0)
            {
                phi[v] = phi[i] * prm[j];
                break;
            }
            phi[v] = phi[i] * (prm[j] - 1);
        }
    }
}
int main()
{
    // freopen("P6570.in", "r", stdin);
    // freopen("P6570.out", "w", stdout);
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    init();
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        f[a[i]]++;
    }
    dp[0] = 1;
    for(int i = 1; i < (1 << 18); i++)
        for(int j = i; j > 0; j = ((j - 1) & i))
            if(j > (i ^ j))
                dp[i] = (dp[i] + dp[i ^ j] * f[j] % mod) % mod;
    dp[0] = qpow(2, f[0]);
    for(int i = 0; i < (1 << 18); i++)
    {
        if(i > 0) dp[i] = (dp[i] * dp[0]) % mod;
        ans = (ans + dp[i] * phi[1 + i] % mod) % mod;
    }
    cout << ans;
    return 0;
}
posted @ 2026-01-19 18:44  KS_Fszha  阅读(2)  评论(0)    收藏  举报