NOIP2024集训 Day37 总结

前言

如果一切不如你所愿,就在尘埃落定前奋力一搏。

今天的题目也是比较快速的做完了。

所以先来总结一下。

今天是计数专题,组合数居多。

以前做过的题目这里就稍稍略过了。

Merge Triplets

观察到对于能够得到的最终的排列 \(p\),对于其中的一个数 \(p_i\),不可能做到 \(p_i>\max_{j=i+1}^{i+3}p_j\)

感觉是比较显然的,这里就不仔细证明了。

为了方便转化这个性质,我们考虑对 \(p\) 求一个前缀 \(\max\) 数组 \(q\),他一定满足最长的连续相同的段 \(\le 3\)

而我们看每一个块对这个连续段的贡献,一定会分为以下三种:

  • 直接贡献一个长度为 \(3\) 的相同段。

  • 贡献三个长度为 \(1\) 的相同段。

  • 贡献一个长度为 \(1\) 的相同段和一个长度为 \(2\) 的相同段。

我们考虑相同段满足哪些条件才是充分的。

首先一个很必要的是所有相同段的长度 \(\le 3\),但是显然满足这个条件的排列不一定能被构造出来。

然后我们观察上面的三种贡献可以得到一个点:长度为 \(1\) 的相同段的个数一定 \(\ge\) 长度为 \(2\) 的相同段。

而这两个条件加起来就是充分的了,也就是说只要满足这两个条件的所有排列都是合法的。

但是显然我们不能把长度为 \(1\) 的相同段和长度为 \(2\) 的相同段的个数都记下来,这显然时间复杂度会炸。

故我们考虑记录长度为 \(1\) 的相同段和长度为 \(2\) 的相同段个数的差

定义 \(dp_{i,j}\) 表示算了 \(i\) 个块的贡献,长度为 \(1\) 的相同段个数 \(-\) 长度为 \(2\) 的相同段个数为 \(j\)

转移如下:

dp[i + 1][j + 1 + m] += dp[i][j + m], dp[i + 1][j + 1 + m] %= mod;
dp[i + 2][j - 1 + m] += dp[i][j + m] * (i + 1ll) % mod, dp[i + 2][j - 1 + m] %= mod;
dp[i + 3][j + m] += dp[i][j + m] * (i + 1ll) % mod * (i + 2) % mod, dp[i + 3][j + m] %= mod;

看上去有点抽象对吧。确实是这样的。

我们以第三行的转移为例来解释。

你可以考虑把他理解成一个树上拓扑序的个数,即我们定义第 \(i\) 个块的第 \(j\) 的数为 \((i,j)\)。那么你考虑将 \((i,1)\)\((i-1,1),(i,2),(i,3)\) 连边,本质上就是表示他比这三个数大。

而你把他理解为拓扑序的方案数就可以得到上述转移。

/*
纵使我发现了不可能x[1]>x[2],x[3],x[4]
我也想不到前缀MAX会相同
qwq
*/
#include <bits/stdc++.h>
using namespace std;
#define maxn 6005
int m = 6000;
int dp[maxn][maxn << 1];
int n, mod;
int main()
{
    cin >> n >> mod;
    n *= 3, dp[0][m] = 1;
    for (int i = 0; i < n; ++i)
    {
        for (int j = -i; j <= i; ++j)
        {
            if(!dp[i][j + m]) continue;
            dp[i + 1][j + 1 + m] += dp[i][j + m], dp[i + 1][j + 1 + m] %= mod;
            dp[i + 2][j - 1 + m] += dp[i][j + m] * (i + 1ll) % mod, dp[i + 2][j - 1 + m] %= mod;
            dp[i + 3][j + m] += dp[i][j + m] * (i + 1ll) % mod * (i + 2) % mod, dp[i + 3][j + m] %= mod;
        }
    }
    int ans = 0;
    for (int j = m; j <= 2 * m; ++j) ans += dp[n][j], ans %= mod;
    cout << ans << endl;
}

Guessing Permutation for as Long as Possible

观察一下这个样例,然后稍微手玩一下大概还是有一些眉目的。

你考虑对于两个二元组 \((i,j),(i,k)\),如果此时 \((j,k)\) 还没有出现的话,那么他们两个的大小比较必须是同号的。

故你把一个边当作点,点权为 \(1\) 即表示 \(a>b\),否则就是 \(b>a\)

而根据刚刚的情况,你就可以直接跑一个 \(\texttt{2-Sat}\),然后看有多少方案。

观察到这个 \(\texttt{2-Sat}\) 比较特殊,是双向边,所以本质上来说,你考虑打一个并查集,而方案数就是 \(2^{连通块的数量}\)

当然稍微判一下 \(0\) 即可。

/*
2-sat 求方案数量板子题
*/
#include <bits/stdc++.h>
using namespace std;
const int maxn = 400 + 5;
int n;
int f[maxn << 1][maxn << 1];
int fa[maxn * maxn << 1];
int find(int x) { return fa[x] == x ? fa[x] : fa[x] = find(fa[x]); }
void unite(int x, int y) { fa[find(x)] = find(y); }
int main()
{
    scanf("%d", &n);
    int m = n * (n - 1) >> 1;
    for (int i = 1; i <= 2 * m; ++i) fa[i] = i;
    for (int i = 1; i <= m; ++i)
    {
        int x, y;
        scanf("%d %d", &x, &y);
        if (x > y) swap(x, y);
        f[x][y] = f[y][x] = i;
        for (int z = 1; z <= n; ++z)
        {
            if (f[x][z] && !f[y][z])
            {
                if (z < x) unite(f[x][z], f[x][y] + m), unite(f[x][z] + m, f[x][y]);
                else unite(f[x][z], f[x][y]), unite(f[x][z] + m, f[x][y] + m);
            }
            if (f[y][z] && !f[x][z])
            {
                if (z > y) unite(f[y][z], f[x][y] + m), unite(f[y][z] + m, f[x][y]);
                else unite(f[y][z], f[x][y]), unite(f[y][z] + m, f[x][y] + m);
            }
        }
    }
    for (int i = 1; i <= m; ++i) if (find(i) == find(i + m)) return puts("0"), 0;
    int ans = 1;
    for (int i = 1; i <= m; ++i) ans <<= (fa[i] == i), ans %= 1000000007;
    printf("%d\n", ans);
    return 0;
}

Yet Another ABC String

今天的一道超级妙妙题,不看题解根本想不到好吧。

同时也启示我们容斥并没有我们想象中的那么局限。

首先,看完这个题面感觉就很容斥。

但是如果你直接去容斥选择了至少 \(i\)\(\texttt{ABC,BCA,CAB}\) 感觉又不是很好做。

我们稍微观察一下这个 \(\texttt{ABC,BCA,CAB}\)

我们发现,如果 \(s_{i,i+1,i+2}\) 为其中的一个,并且 \(s_{i+2,i+3,i+4}\) 也为其中的一个,那么 \(s_{i+1,i+2,i+3}\) 也显然是其中的一个,也就是不合法的。

这启示我们他具有传递性。

于是我们可以考虑对这个不合法的字符串进行一个极长处理。

即考虑对极长的不合法的子串进行容斥。(这个极长的就是说对于里面任意长度为 \(3\) 子串,他都是不合法的,而要判断一个字符串是否是满足这个条件的只需要看第一个和最后一个长度为 \(3\) 的子串是否都不合法。)

故我们考虑去枚举至少有 \(i\) 个不合法的极长子串。

虽然我们不知道这些子串的长度,但是它里面的前三个必然是由 \(1\times A,1\times B,1\times C\) 组成的。

也就是说在用最少的字符去满足这个子串之后,我们只剩下了 \(a+b+c-3\times i\) 个字符,其中 \(A\times (a-i),B\times (b-i),C\times (c-i)\)

我们考虑对这些字符进行乱排,方案数就是 \(C_{a+b+c-3\times i}^{a-i}\times C_{b+c-2\times i}^{b-i}\)

然后考虑将我们取出来的这 \(i\) 个不合法的长度为 \(3\) 的子串插入进去。

插入的时候,如果你在首位,那是没什么限制的。但是如果是在中间,那么比方说上一个字符是 \(B\),那么此时你就只能将 \(ABC,BCA\) 插入进去,不然就可能传递到上一个插入的位置导致这个不合法的极长子串数量到不了 \(i\)

而观察到如果插入在中间的话,不管上一个字符是什么,都只有两种长度为 \(3\) 的子串可以插入进去。

故我们将插入方案分为两类:

  • 有在首位插入的。即将 \(i-1\) 个球放入 \(a+b+c-3\times i+1\)(注意开头的后面也是可以接的)个盒子中,可以有空盒的方案数 \(\times 2^{i-1}\times 3\)。(\(2^{i-1}\) 表示这些位置每个位置都有两种不合法的子串可以填,下同)

  • 没有在首位插入的。即将 \(i\) 个球放入 \(a+b+c-3\times i\) 个盒子的方案数 \(\times 2^i\)

然后注意一下容斥系数即可。

可以观察到这样去钦定极长不合法子串的个数可以使得我们在插入的时候不需要管后面而只需要管会不会和前面的接在一起,就算和后面成为了新的极长不合法子串但是再怎么极长也牵扯不到后面一个插入的位置从而达到至少有 \(i\) 个的效果。

更多细节见代码:

/*
神仙题!!!
谁容斥会这么想啊/lh /lh
*/
#include <bits/stdc++.h>
using namespace std;
#define maxn 3000005
const int mod = 998244353;
int ksm(int x, int y)
{
    int sum = 1;
    while(y)
    {
        if(y & 1) sum = 1ll * sum * x % mod;
        y >>= 1, x = 1ll * x * x % mod;
    }
    return sum;
}
int a, b, c;
int fact[maxn], inv[maxn];
int C(int x, int y)
{
    return fact[x] * 1ll * inv[y] % mod * inv[x - y] % mod;
}
int main()
{
    fact[0] = 1;
    for (int i = 1; i <= maxn - 5; ++i) fact[i] = 1ll * fact[i - 1] * i % mod;
    for (int i = 0; i <= maxn - 5; ++i) inv[i] = ksm(fact[i], mod - 2);
    cin >> a >> b >> c;
    int n = a + b + c;
    int ans = C(n, a) * 1ll * C(n - a, b) % mod, flg = 1;
    for (int i = 1; i <= min(a, min(b, c)); ++i)
    {
        flg = -flg;
        int x = n - 3 * i;
        int y = C(x, a - i) * 1ll * C(x - a + i, b - i) % mod;
        ans += flg * 1ll * y % mod * ((C(x + i, i) * 1ll * ksm(2, i) % mod + C(x + i - 1, i - 1) * 1ll * ksm(2, i - 1) % mod) % mod) % mod;
        ans %= mod, ans += mod, ans %= mod;
    }
    cout << ans << endl;
}

Tricolor Pyramid

一个比较巧妙但是又不算非常难想的转化是,你把三个字母分别看作 \(0,1,2\),一次合并就是在模 \(3\) 的意义下计算 \(3-i-j\)

故你只需要计算最初状态一个点对最终状态的贡献即可,其实就是 \(C_{n-1}^{i-1}\),然后注意一下你减来减去的正负号即可。

细节见代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 4e5 + 5;
ll n;
char s[maxn];
ll C[5][5];
ll con(ll n, ll m)
{
    if (m > n) return 0;
    return C[n][m];
}
ll lucas(ll n, ll m)
{
    if (n < 3 && m < 3) return con(n, m);
    return lucas(n / 3, m / 3) * con(n % 3, m % 3);
}
int main()
{
    cin >> n;
    cin >> (s + 1);
    C[0][0] = 1;
    C[1][0] = C[1][1] = 1;
    C[2][0] = 1, C[2][1] = 2, C[2][2] = 1;
    ll ans = 0;
    for (int i = 1; i <= n; i++)
    {
        if (s[i] == 'B') ans = (ans + lucas(n - 1, i - 1) * 0) % 3;
        if (s[i] == 'R') ans = (ans + lucas(n - 1, i - 1) * 1) % 3;
        if (s[i] == 'W') ans = (ans + lucas(n - 1, i - 1) * 2) % 3;
    }
    if (n % 2 == 0) ans = 3 - ans;
    ans %= 3;
    if (ans == 0) cout << 'B';
    if (ans == 1) cout << 'R';
    if (ans == 2) cout << 'W';
    return 0;
}

Add and Remove

观察到你对一个区间操作完之后,剩下的一定是两个端点。

但是你并不知道这个端点在接下来的操作会被计算多少次。

于是你考虑定义 \(dp_{l,r,x,y}\) 表示操作完 \([l,r]\) 之后,左端点被计算 \(x\) 次,右端点被计算 \(y\) 次的答案最小是多少。

转移比较显然的,枚举 \(l<k<r\)\(dp_{l,r,x,y}=\min(dp_{l,k,x,x+y}+dp_{k,r,x+y,y}+(a_k\times(x+y)))\)

但是有一个小问题,就是你这个 \(x,y\) 会非常大,而你只需要知道 \(dp_{1,n,1,1}\) 的值。

故你考虑对这个状态直接爆搜,因为 \(n\) 比较小,所以是不会炸的。

#include <bits/stdc++.h>
using namespace std;
#define maxn 20
int n;
int a[maxn];
long long dfs(int l, int r, int x, int y)
{
    if(r - l == 1) return 0;
    long long ans = LONG_LONG_MAX;
    for (int k = l + 1; k < r; ++k) ans = min(ans, dfs(l, k, x, x + y) + dfs(k, r, x + y, y) + a[k] * 1ll * (x + y));
    return ans;
}
int main()
{
    cin >> n;
    for (int i = 1; i <= n; ++i) cin >> a[i];
    cout << dfs(1, n, 1, 1) + a[1] + a[n] << endl;
    return 0;
}

Square Constraints

其实你看见这个 \(a^2+b^2\) 是比较容易想到圆方程的,当然这个不是很重要。

你可以非常轻松的求出第 \(i\) 个数可以取到的上下界:\(l_i=\sqrt{n^2-i^2},r_i=\sqrt{4\times n^2-i^2}\),当然下界是向上取整,上界是向下取整。

题目就变成了问你有多少个排列 \(p\),满足 \(p_i\in[l_i,r_i]\)

为了方便理解可以把 \((i,p_i)\) 看作二维坐标系上的点,而每行只能有一个点。

考虑如果没有这个 \(p_i\) 的限制的话,由于 \(r_i\) 是单调不增的,你可以倒着选,每选一次都会让接下来可选的位置少 \(1\)

方案数就是 \(\prod_{i=1}^{n}r_{n-i+1}-i+1\)

我们得到的启示是,我们在选点的时候,我们是尽量希望从范围小的选到范围大的,这样前面对后面的影响就一目了然。

并且,如果这个比较好计算的话,那我们考虑容斥。

即定义至少有 \(k\) 个不合法,即 \(\in[l_i-1]\)

考虑一个 \(dp_{i,j}\) 表示在选择确定了 \(i\) 个点之后至少有 \(j\) 个点不合法的方案数。

这个直接按照从左到右的顺序计算的话仍然是非常困难的,因为这个状态你根本无法知道前面会对后面的选择究竟产生什么贡献。

此时我们就要根据刚刚得到的启示去改变这个顺序。

首先观察到,对于 \(i\in[n,2\times n)\) 的点怎么都是合法的,因为他的 \(l_i=0\)

如果 \(i\in [n,2\times n)\),那么会影响到他的点就有:

  • \(j\in[0,n),l_j-1\le r_i\),并且钦定了 \(j\) 不合法。

  • \(j\in [n,2\times n),r_j\le r_i\)

如果 \(i\in [0,n)\),如果我钦定他不合法,那么会影响到他的点就有:

  • \(j\in[0,n),l_j-1\le l_i-1\),并且钦定了 \(j\) 不合法。

  • \(j\in [n,2\times n),r_j\le l_i-1\)

如果 \(i\in [n,2\times n)\),如果我钦定他合法,那么会影响到他的点就有:

  • \(j\in[0,n),l_j-1\le r_i\),钦定了 \(j\) 不合法的点。

  • \(j\in[0,n),r_j\le r_i\),钦定了 \(j\) 合法的点。

  • \(j\in [n,2\times n)\) 中的所有点。(正是因为这个东西的存在,导致就算这个 \(j\in[n,2\times n\) 排在了 \(i\) 后面,但是他的影响是仍然可以直接算出来的,并不在乎他排在什么位置。)

我们考虑对这些点进行排序,第一关键字即 \(i\in[0,n)\to l_i-1,i\in[n,2\times n)\to r_i\),第二关键字就是编号。

而这个时候我们的方案就是可以统计的了。

因为前面对后面产生的贡献都是可以直接计算的了。

具体转移就见代码吧,感觉没啥好讲的。

/*
妙的,但是感觉是我自己没有想的很认真(?
主打一个通过巧妙的排序让前面不对后面产生影响。
*/
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 510;
const int inf = 1e9;
int n, mod;
int dp[maxn][maxn];
vector<pair<int, int>> lim;
int solve(int k)
{
    memset(dp, 0, sizeof(dp));
    dp[0][0] = 1;
    int lcnt = 0, rcnt = 0;
    for (int i = 1; i <= (n << 1); i++)
    {
        if (lim[i].second == 0)
        {
            for (int j = 0; j <= k; j++) dp[i][j] = (dp[i][j] + 1ll * dp[i - 1][j] * (lim[i].first + 1 - rcnt - j) % mod) % mod;
            rcnt++;
        }
        else
        {
            for (int j = 0; j <= k; j++)
            {
                dp[i][j] = (dp[i][j] + 1ll * dp[i - 1][j] * (lim[i].second + 1 - k - (lcnt - j) - n) % mod) % mod;
                if (j) dp[i][j] = (dp[i][j] + 1ll * dp[i - 1][j - 1] * (lim[i].first - j - rcnt + 2) % mod) % mod;
            }
            lcnt++;
        }
    }
    return dp[n << 1][k];
}
void init()
{
    lim.emplace_back(-inf, -inf);
    for (int i = 0; i < n; i++)
    {
        int l = ceil(sqrt((long double)n * n - i * i));
        int r = min(2 * n - 1, (int)floor(sqrt(4 * n * n - i * i)));
        lim.emplace_back(l - 1, r);
    }
    for (int i = n; i < 2 * n; i++)
    {
        int r = min(2 * n - 1, (int)floor(sqrt(4 * n * n - i * i)));
        lim.emplace_back(r, 0);
    }
    sort(lim.begin(), lim.end());
}
int main()
{
    cin >> n >> mod;
    init();
    int ans = 0;
    for (int i = 0; i <= n; i++)
    {
        int del = solve(i);
        if (i & 1) ans = (ans - del + mod) % mod;
        else ans = (ans + del) % mod;
    }
    cout << ans;
}

后记

大功告成了,今天也是收获颇丰的一天。

\(\texttt{Dawn breaks, painting the world with hues of hope.}\)

posted @ 2024-09-24 14:58  Saltyfish6  阅读(30)  评论(0)    收藏  举报
Document