NOIP2024集训 Day37 总结
前言
如果一切不如你所愿,就在尘埃落定前奋力一搏。
今天的题目也是比较快速的做完了。
所以先来总结一下。
今天是计数专题,组合数居多。
以前做过的题目这里就稍稍略过了。
Merge Triplets
观察到对于能够得到的最终的排列
感觉是比较显然的,这里就不仔细证明了。
为了方便转化这个性质,我们考虑对
而我们看每一个块对这个连续段的贡献,一定会分为以下三种:
-
直接贡献一个长度为
的相同段。 -
贡献三个长度为
的相同段。 -
贡献一个长度为
的相同段和一个长度为 的相同段。
我们考虑相同段满足哪些条件才是充分的。
首先一个很必要的是所有相同段的长度
然后我们观察上面的三种贡献可以得到一个点:长度为
而这两个条件加起来就是充分的了,也就是说只要满足这两个条件的所有排列都是合法的。
但是显然我们不能把长度为
故我们考虑记录长度为
定义
转移如下:
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;
看上去有点抽象对吧。确实是这样的。
我们以第三行的转移为例来解释。
你可以考虑把他理解成一个树上拓扑序的个数,即我们定义第
而你把他理解为拓扑序的方案数就可以得到上述转移。
/*
纵使我发现了不可能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
观察一下这个样例,然后稍微手玩一下大概还是有一些眉目的。
你考虑对于两个二元组
故你把一个边当作点,点权为
而根据刚刚的情况,你就可以直接跑一个
观察到这个
当然稍微判一下
/*
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
今天的一道超级妙妙题,不看题解根本想不到好吧。
同时也启示我们容斥并没有我们想象中的那么局限。
首先,看完这个题面感觉就很容斥。
但是如果你直接去容斥选择了至少
我们稍微观察一下这个
我们发现,如果
这启示我们他具有传递性。
于是我们可以考虑对这个不合法的字符串进行一个极长处理。
即考虑对极长的不合法的子串进行容斥。(这个极长的就是说对于里面任意长度为
故我们考虑去枚举至少有
虽然我们不知道这些子串的长度,但是它里面的前三个必然是由
也就是说在用最少的字符去满足这个子串之后,我们只剩下了
我们考虑对这些字符进行乱排,方案数就是
然后考虑将我们取出来的这
插入的时候,如果你在首位,那是没什么限制的。但是如果是在中间,那么比方说上一个字符是
而观察到如果插入在中间的话,不管上一个字符是什么,都只有两种长度为
故我们将插入方案分为两类:
-
有在首位插入的。即将
个球放入 (注意开头的后面也是可以接的)个盒子中,可以有空盒的方案数 。( 表示这些位置每个位置都有两种不合法的子串可以填,下同) -
没有在首位插入的。即将
个球放入 个盒子的方案数 。
然后注意一下容斥系数即可。
可以观察到这样去钦定极长不合法子串的个数可以使得我们在插入的时候不需要管后面而只需要管会不会和前面的接在一起,就算和后面成为了新的极长不合法子串但是再怎么极长也牵扯不到后面一个插入的位置从而达到至少有
更多细节见代码:
/*
神仙题!!!
谁容斥会这么想啊/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
一个比较巧妙但是又不算非常难想的转化是,你把三个字母分别看作
故你只需要计算最初状态一个点对最终状态的贡献即可,其实就是
细节见代码:
#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
观察到你对一个区间操作完之后,剩下的一定是两个端点。
但是你并不知道这个端点在接下来的操作会被计算多少次。
于是你考虑定义
转移比较显然的,枚举
但是有一个小问题,就是你这个
故你考虑对这个状态直接爆搜,因为
#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
其实你看见这个
你可以非常轻松的求出第
题目就变成了问你有多少个排列
为了方便理解可以把
考虑如果没有这个
方案数就是
我们得到的启示是,我们在选点的时候,我们是尽量希望从范围小的选到范围大的,这样前面对后面的影响就一目了然。
并且,如果这个比较好计算的话,那我们考虑容斥。
即定义至少有
考虑一个
这个直接按照从左到右的顺序计算的话仍然是非常困难的,因为这个状态你根本无法知道前面会对后面的选择究竟产生什么贡献。
此时我们就要根据刚刚得到的启示去改变这个顺序。
首先观察到,对于
如果
-
,并且钦定了 不合法。 -
如果
-
,并且钦定了 不合法。 -
如果
-
,钦定了 不合法的点。 -
,钦定了 合法的点。 -
中的所有点。(正是因为这个东西的存在,导致就算这个 排在了 后面,但是他的影响是仍然可以直接算出来的,并不在乎他排在什么位置。)
我们考虑对这些点进行排序,第一关键字即
而这个时候我们的方案就是可以统计的了。
因为前面对后面产生的贡献都是可以直接计算的了。
具体转移就见代码吧,感觉没啥好讲的。
/*
妙的,但是感觉是我自己没有想的很认真(?
主打一个通过巧妙的排序让前面不对后面产生影响。
*/
#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;
}
后记
大功告成了,今天也是收获颇丰的一天。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】