The 2024 Hunan Multi-School Programming Training Contest, Round 4
写在前面
比赛地址:https://codeforces.com/gym/514727
搬运自:ICPC 2021 ASIA YOKOHAMA REGIONAL
以下按个人向难度排序。
赛时写三题挂三题太唐了
以及霓虹人的题解写得什么 b 东西根本不讲代码实现的是吧
A
签到。
保证每个球与两个球相交,于是枚举每对球按照公式计算其相交体积即可。
妈的圆周率精确到小数点后 15 位才能过。
可以用 acos(-1)
替代圆周率,然而赛时真的手打了 15 位,我是麻瓜。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 110;
const double eps = 1e-9;
//=============================================================
int n;
double ans, r, with;
double x[kN], y[kN], z[kN];
double inter[kN][kN];
//=============================================================
double distance(int a_, int b_) {
return sqrt((x[b_] - x[a_]) * (x[b_] - x[a_]) +
(y[b_] - y[a_]) * (y[b_] - y[a_]) +
(z[b_] - z[a_]) * (z[b_] - z[a_]));
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> r;
for (int i = 1; i <= n; ++ i) {
std::cin >> x[i] >> y[i] >> z[i];
ans += 4.0 * acos(-1) * r * r * r / 3.0;
for (int j = 1; j < i; ++ j) {
double d = distance(i, j);
if (d >= 2.0 * r - eps) continue;
inter[i][j] = 2.0 * acos(-1) * (r - d / 2.0) * (r - d / 2.0) * (2.0 * r + d / 2.0) / 3.0;
ans -= inter[i][j];
// std::cout << i << j << " " << inter[i][j] << "\n";
}
}
std::cout << std::fixed << std::setprecision(9) << ans - with << "\n";
return 0;
}
B
枚举,贪心。
大力枚举可能的串的后缀。
Code by dztle:
#include<bits/stdc++.h>
using namespace std;
#define int long long
inline int read(){
int x=0,f=1; char s;
while((s=getchar())<'0'||s>'9') if(s=='-') f=-1;
while(s>='0'&&s<='9') x=(x*10)+(s^'0'),s=getchar();
return x*f;
}
const int N=100005;
int n;
char s[10];
int se[105],tt[105],ok[105];
int tic[1000005];
int cnt[10006];
int q[105],top;
signed main(){
cin>>n;
for(int i=1;i<=n;++i){
scanf("%s",s+1);
int num=0,num2=0,num3=0;
for(int j=5;j<=6;++j){
num=num*10+(s[j]-'0');
}
tt[num]++;
for(int j=3;j<=6;++j){
num2=num2*10+(s[j]-'0');
}
cnt[num2]++;
se[num]=max(se[num],cnt[num2]);
for(int j=1;j<=6;++j){
num3=num3*10+(s[j]-'0');
}
tic[num3]++;
}
for(int i=0;i<=1000000;++i){
if(tic[i]==1){
ok[i%100]=1;
}
}
int ioio=0;
for(int i=0;i<=99;++i){
if(se[i]) ++ioio;
}
int ans=0;
if(ioio>=1){
for(int i=0;i<=99;++i){
if(ok[i]) ans=max(ans,(int)300000);
ans=max(ans,4000*se[i]);
}
top=0;
for(int k=0;k<=99;++k){
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,500*opop);
}
if(ioio>=2){
for(int i=0;i<=99;++i){
if(tt[i]){
for(int j=0;j<=99;++j){
if(i!=j&&tt[j]){
if(ok[i]) ans=max(ans,300000+4000*se[j]);
}
}
}
}
for(int i=0;i<=99;++i){
if(!ok[i]) continue;
if(tt[i]){
top=0;
for(int k=0;k<=99;++k){
if(i==k) continue;
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,300000+500*opop);
}
}
for(int i=0;i<=99;++i){
if(tt[i]){
top=0;
for(int k=0;k<=99;++k){
if(i==k) continue;
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,4000*se[i]+500*opop);
}
}
// cout<<ans<<endl;
}
if(ioio>=3){
for(int i=0;i<=99;++i){
if(!ok[i]) continue;
if(tt[i]){
for(int j=0;j<=99;++j){
if(i==j) continue;
if(tt[j]){
top=0;
for(int k=0;k<=99;++k){
if(j==k) continue;
if(i==k) continue;
if(tt[k]){
q[++top]=tt[k];
}
}
sort(q+1,q+1+top);
int opop=0;
for(int k=top;k>=top-2;--k){
if(k==0) break;
opop+=q[k];
}
ans=max(ans,300000+4000*se[j]+500*opop);
}
}
}
}
}
cout<<ans;
return 0;
}
/*
7
034207
924837
372745
382947
274637
083907
294837
*/
J
枚举,二分。
首先预处理出所有左下无其他点的点集 \(\mathbf{BL}\),右上无其他点的点集 \(\mathbf{TR}\),显然合法点对 \((u, v)(x_u < x_v)\) 必有 \(u\in \mathbf{BL}, v\in \mathbf{TR}\)。根据性质可发现,当这两个点集中的点按 \(x\) 排序后,\(y\) 均为递减的。
考虑钦定 \(u\in \mathbf{BL}, v\in \mathbf{TR}\) 前提下如何判断点对 \((u, v)\) 合法。
- 此时 \(u\) 左下 与 \(v\) 右上均没有其他点,考虑另外两个未被包含的区域中是否有点。
- 另外两个未被包含的区域分别为:\((x< x_{u} \land y > y_v)\),\((x > x_v \land y < y_u)\)。
- 对于 \((x < x_u \land y > y_v)\):则应有所有 \(y > y_v\) 的点均满足 \(x > x_u\)。
- 对于 \((x > x_v \land y < y_u)\):则应有所有 \(x > x_v\) 的点均满足 \(y > y_u\)。
- 满足上述两条件则未被包含的区域中无其他点。
考虑对 \(v\in \mathbf{TR}\) 预处理 \(\operatorname{lim_x}(v) = \min\{ x | (x, y) \in \land y > y_v \}\),\(\operatorname{lim_y}(v) = \max\{ y | (x, y)\in S \land x > x_v\}\),则对于 \(u\in \mathbf{BL}\),\((u, v)\) 合法当且仅当:\(x_u< \operatorname{lim_x}(v) \land y_u< \operatorname{lim_y}(v)\),即有下图:
由上述性质,发现若将 \(\mathbf{BL}\) 中的点按 \(x\) 递增排序,则对于任意 \(v\in \mathbf{TR}\),能与其构成合法点对的 \(u\) 在 \(\operatorname{BL}\) 中一定构成一段连续的区间。于是在枚举 \(v\) 过程中二分求得合法区间中的点的数量即可。
预处理 \(\mathbf{BL}, \mathbf{TR}\) 即二维偏序问题,树状数组预处理即可。预处理 \(\operatorname{lim_x}\) 与 \(\operatorname{lim_y}\) 均为 \(O(n)\) 级别,单次询问合法区间时间复杂度 \(O(\log n)\) 级别。则总时间复杂度 \(O(n\log n)\) 级别。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e6 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int n;
struct Point {
int x, y, id;
} pt[kN], ptx[kN], pty[kN];
int posx[kN], posy[kN];
int xlimit[kN], ylimit[kN];
std::vector <Point> bottom_left;
LL ans;
//=============================================================
namespace BIT {
#define lowbit(x) ((x)&(-x))
const int kNode = kN;
int lim, t[kNode];
void Init(int lim_) {
lim = lim_;
memset(t, 0, sizeof (t));
}
void Insert(int pos_) {
for (int i = pos_; i <= lim; i += lowbit(i)) {
++ t[i];
}
}
int Sum(int pos_) {
int ret = 0;
for (int i = pos_; i; i -= lowbit(i)) ret += t[i];
return ret;
}
int Query(int l_, int r_ = lim) {
if (l_ > r_) return 0;
return Sum(r_) - Sum(l_ - 1);
}
}
bool cmpx(Point fir_, Point sec_) {
return fir_.x < sec_.x;
}
bool cmpy(Point fir_, Point sec_) {
return fir_.y < sec_.y;
}
void Init() {
std::sort(ptx + 1, ptx + n + 1, cmpx);
std::sort(pty + 1, pty + n + 1, cmpy);
xlimit[n] = kInf, ylimit[n] = kInf;
for (int i = n; i; -- i) {
posx[ptx[i].id] = i, posy[pty[i].id] = i;
if (i < n) {
xlimit[i] = pty[i + 1].x, ylimit[i] = ptx[i + 1].y;
xlimit[i] = std::min(xlimit[i], xlimit[i + 1]);
ylimit[i] = std::min(ylimit[i], ylimit[i + 1]);
}
}
BIT::Init(1e6 + 5);
for (int i = 1; i <= n; ++ i) {
if (!BIT::Query(1, ptx[i].y - 1)) {
bottom_left.push_back(ptx[i]);
}
BIT::Insert(ptx[i].y);
}
}
void Query(Point pt_) {
int xl = xlimit[posy[pt_.id]], yl = ylimit[posx[pt_.id]];
int posl = bottom_left.size(), posr = -1;
for (int l = 0, r = bottom_left.size() - 1; l <= r; ) {
int mid = (l + r) >> 1;
if (bottom_left[mid].x < xl) {
posr = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
for (int l = 0, r = bottom_left.size() - 1; l <= r; ) {
int mid = (l + r) >> 1;
if (bottom_left[mid].y < yl) {
posl = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
if (posl <= posr) ans += posr - posl + 1;
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; ++ i) {
int x, y; std::cin >> x >> y;
ptx[i] = pty[i] = pt[i] = (Point) {x, y, i};
}
Init();
BIT::Init(1e6 + 5);
for (int i = n; i; -- i) {
if (!BIT::Query(pty[i].x + 1)) {
Query(pty[i]);
// if (ans) std::cout << pty[i].x << " " << pty[i].y << "\n";
}
BIT::Insert(pty[i].x);
}
std::cout << ans << "\n";
return 0;
}
D
分治,DP。
手玩下发现,若确定了最终的树的根节点 \(m\),则 \(m\) 为树中高度最高的位置,且树的形态一定是以下两种之一:
case-1:直径跨越根节点
case-2:直径不跨越根节点
对于第一种情况,发现仅需要考虑左右两侧可构成的最长的链并将它们连向 \(m\);对于第二种情况,发现等价于将某侧的一棵树从根节点处断开,并将两条链均连向 \(m\)。发现可以通过两侧可构成的树转移得到以 \(m\) 为根的树,于是考虑维护区间可构成的树的形态并进行转移。
设 \(\operatorname{D}(l, r, m)\) 表示由区间 \([l, r]\) 构成的根为 \(m\) 的树的最长直径,\(\operatorname{H}(l, r, m)\) 表示由区间 \([l, r]\) 构成的根为 \(m\) 的最长链的长度,初始化 \(\operatorname{D}(i, i, i) = \operatorname{H}(i, i, i) = 0\)。
根据建树时的操作限制,需要限制状态 \((l, r, m)\) 中满足区间 \([l, r]\) 中 \(h_m\) 为最大值。然后考虑上述两种情况有转移:
则答案即为 \(\operatorname{D}(1, n, \operatorname{pos}_n)\)。
然而这转移是 \(O(n^4)\) 的不可能跑过去啊呃呃。
发现其中有很多无用状态。
- 对于区间 \([l, r]\) 显然在转移时当且仅当 \(m\) 为区间中最大值位置时,\(\operatorname{D}(l, r, m), \operatorname{H}(l, r, m)\) 才会有贡献,于是不需要记状态中根节点的位置,直接减一维状态。
- 更进一步地,记 \(L_i\) 为 \(i\) 向左第一个大于 \(h_i\) 的位置,\(R_i\) 为向右第一个大于 \(h_i\) 的位置。发现以 \(m\) 为根节点的状态中,仅有 \((L_m + 1, R_m - 1, m)\) 这一个状态是有贡献的,其他状态均被包含在了该状态中,使用其他状态并不会使转移更优。
发现实际上仅有 \(n\) 个状态 \((L_i + 1, R_i - 1, i)\) 是有用的,且转移时一定是按照 \(h\) 递增转移的,于是考虑按照 \(h\) 升序枚举根节点与所在区间进行转移。可直接递归地分治实现。在进行区间转移时需要求得区间内的最大值,ST 表实现即可。
仅有 \(O(n)\) 个状态则仅需进行 \(O(n)\) 次求区间最值操作,则总时间复杂度为 \(O(n\log n)\) 级别。
实现时甚至并不需要显式地定义上述状态,详见代码。
使用单调栈按照上述思路进行转移可以做到线性。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 1e6 + 10;
//=============================================================
int n, a[kN];
//=============================================================
namespace ST {
int mylog2[kN], f[kN][22], g[kN][22];
void Init() {
mylog2[1] = 0;
for (int i = 1; i <= n; ++ i) {
f[i][0] = a[i], g[i][0] = i;
if (i > 1) mylog2[i] = mylog2[i >> 1] + 1;
}
for (int i = 1; i <= 20; ++ i) {
for (int j = 1; j + (1 << i) - 1 <= n; ++ j) {
if (f[j][i - 1] > f[j + (1 << (i - 1))][i - 1]) {
f[j][i] = f[j][i - 1], g[j][i] = g[j][i - 1];
} else {
f[j][i] = f[j + (1 << (i - 1))][i - 1];
g[j][i] = g[j + (1 << (i - 1))][i - 1];
}
}
}
}
int Query(int l_, int r_) {
int len = mylog2[r_ - l_ + 1];
if (f[l_][len] > f[r_ - (1 << len) + 1][len]) {
return g[l_][len];
}
return g[r_ - (1 << len) + 1][len];
}
}
pii Solve(int L_, int R_) {
if (L_ > R_) return mp(-kN, -kN);
if (L_ == R_) return mp(0, 0);
int top = ST::Query(L_, R_), d = 0, h = 0;
pii retl = Solve(L_, top - 1), retr = Solve(top + 1, R_);
d = std::max(std::max(retl.first, retr.first), retl.second + retr.second + 1) + 1;
h = std::max(retl.second, retr.second) + 1;
return mp(d, h);
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; ++ i) std::cin >> a[i];
ST::Init();
pii ans = Solve(1, n);
std::cout << ans.first << "\n";
return 0;
}
C
图论转化。
发现这个题目的操作莫名奇妙的啊、、、需要打成目标状态同时还要最优化,然而操作数量不算多,且操作后的状态也不多、、、于是考虑能否图论转化,将构造最优化的方案,转化为求图上的最短路径。
考虑记节点 \((i, j) (0\le i,j\le n)\) 表示通过某些操作可正向构造出 \(s[1:i]\),且这些操作在反向时会构造出 \(s[n-j+1:n]\),将往操作序列后添加一对 \(A_i L_i\) 视为节点间的单向边,则答案即为构造一条从 \((0, 0)\) 到 \((n, n)\) 的字典序最小的最短路径。
建图时枚举节点再枚举至多 \(10^2\) 种出边,大力讨论+尝试匹配即可,详见代码。发现节点的出边仅会连向 \(i, j\) 更大的节点,实际上是一张 DAG,在 DAG 上按操作的字典序贪心地拓扑排序即可直接求得最短路径。
共有 \((n+1)^2\) 个节点,每个节点至多有 \(10^2\) 条出边。时间复杂度瓶颈在于预处理时对于出边需要大力匹配得到下一状态,则总时间复杂度 \(O\left(10^3\times (n+1)^2\right)\) 级别。
以下代码因为用了 vector
存图常数较大跑了 999ms,差点就出来了(((
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
#define pii std::pair<int,int>
#define mp std::make_pair
const int kN = 510;
const int kInf = 1e9 + 2077;
//=============================================================
int n;
std::string s;
std::vector<int> to[kN * kN], wgt[kN * kN];
int from[kN * kN], f[kN * kN], end[kN * kN];
bool vis[kN * kN];
//=============================================================
int id(int x_, int y_) {
return x_ * (n + 1) + y_;
}
int getnext(int i_, int j_, int x_, int y_) {
if (x_ != 0 && y_ != 0 && i_ < x_) return -1;
if (x_ != 0 && y_ != 0 && n - j_ - x_ < y_) return -1;
int nexti = (x_ == 0 ? i_ + 1 : i_ + y_);
int nextj = (y_ == 0 ? j_ + 1 : j_ + x_);
if (x_ == 0 && s[i_] != (char)('0' + y_)) return -1;
for (int k = i_; k <= nexti - 1; ++ k) {
if (s[k] != s[k - x_]) return -1;
}
if (y_ == 0 && s[n - j_ - 1] != (char)('0' + x_)) return -1;
for (int k = n - nextj - 1 + 1; k <= n - j_ - 1; ++ k) {
if (s[k] != s[k - y_]) return -1;
}
return id(nexti, nextj);
}
void Init() {
for (int i = 0; i <= n; ++ i) {
for (int j = 0; j <= n; ++ j) {
f[id(i, j)] = kInf;
for (int a = 0; a <= 9; ++ a) {
for (int l = 0; l <= 9; ++ l) {
int next = getnext(i, j, a, l);
if (next == -1) continue;
to[id(i, j)].push_back(next);
wgt[id(i, j)].push_back(10 * a + l);
}
}
}
}
}
void Topsort() {
std::queue <int> q;
f[0] = 0;
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
if (vis[u]) continue;
vis[u] = true;
for (int i = 0, sz = to[u].size(); i < sz; ++ i) {
if (f[to[u][i]] > f[u] + 2) {
f[to[u][i]] = f[u] + 2;
from[to[u][i]] = u;
end[to[u][i]] = wgt[u][i];
q.push(to[u][i]);
}
}
}
}
void Getans(int u_) {
if (!u_) return ;
Getans(from[u_]);
std::cout << (char)('0' + end[u_] / 10) << (char)('0' + end[u_] % 10);
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> s;
n = s.length();
Init();
Topsort();
Getans(id(n, n));
return 0;
}
/*
00000
00122100
*/
H
随机化。
有点脱线的题。
可能的序列形态太多了!看起来不太好枚举所有可匹配的字符串并检查。然而这个时限看起来相当宽松,考虑随机化。
然而直接大力无脑蒙特卡洛在答案概率很小时不容易随机到可匹配的字符串,答案很容易变成 0 于是挂掉。
于是考虑缩小样本空间,记:
- 集合 \(G = \{ (i, S) | i = \min \{ j | S \text{ matches } P_j \} \}\),其中的 \((S, i)\) 为字符串 \(S\) 与它可以匹配的编号最小的模式串的二元组,则保证了可匹配的字符串与模式串为单射关系。则答案即为 \(\frac{|G|}{4^n}\)。
- 集合 \(U = \{ (i, S) | S \text{ matches } P_i\}\),即所有可与模式串匹配的字符串与匹配对象的二元组。
则答案即为:
\(|U|\) 容易计算,仅需在读入时统计每个模式串中 ?
数量即可。
考虑通过蒙特卡洛方法近似计算计算 \(\frac{|G|}{|U|}\)。首先在 \(U\) 中等概率随机一个被匹配串 \(S_i\) 并随机构造出一个可与其匹配的 \(T\),然后检查 \(T\) 是否位于 \(G\) 中,也即是否不与 \(1\sim i - 1\) 匹配即可。
关于随机次数的与复杂度证明详见:https://satori5ama.github.io/posts/2024-hunan-multi-school-4/。
话说蒙特卡洛的一个经典应用就是求给定平面不规则图形面积:选择一个完全包含不规则图形 \(S\) 的可计算面积的规则图形 \(S'\),然后往该规则图形上等概率随机若干点并计算位于不规则图形中的点的比例 \(k\),不规则图形面积即可近似看做 \(S = k \times S'\)、、、
唉不会随机化我是麻瓜、、、官方题解说这种方法叫做 FPRAS (fully polynomial-time randomized approximation scheme),然而在中文互联网上直接搜搜不到、、、
注意 __int128
。
//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL __int128
// #define LL long long
const int kN = 110;
const int kMaxtimes = 3e5;
const char kDNA[] = "ACGT";
//=============================================================
int n, m;
std::string s[kN];
double ans1, ans2;
LL sumu[kN];
//=============================================================
LL random(LL mod_) {
return (LL)(((double) rand()) / RAND_MAX * mod_);
}
bool check(int pos_, std::string &t_) {
for (int i = 0; i < n; ++ i) {
if (s[pos_][i] != '?' && s[pos_][i] != t_[i]) return false;
}
return true;
}
//=============================================================
int main() {
//freopen("1.txt", "r", stdin);
srand(time(0));
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
std::cin >> s[i];
double num = 1.0;
sumu[i] = 1;
for (auto ch: s[i]) {
if (ch == '?') sumu[i] *= 4ll;
else num /= 4.0;
}
sumu[i] += sumu[i - 1];
ans1 += num;
}
for (int i = 1; i <= kMaxtimes; ++ i) {
//非等概率选取:
// int p = rand() % m + 1;
int p = std::lower_bound(sumu + 1, sumu + 1 + m, random(sumu[m])) - sumu;
std::string t = s[p];
for (int i = 0; i < n; ++ i) {
if (t[i] == '?') t[i] = kDNA[rand() % 4];
}
int flag = 1;
for (int i = 1; i < p; ++ i) {
if (check(i, t)) {
flag = 0;
break;
}
}
if (flag) ++ ans2;
}
std::cout << std::setprecision(15) << ans1 * ans2 / kMaxtimes << "\n";
return 0;
}
G
DP。
有趣的树计数 DP。
一个显然的想法是考虑按照编号递减添加节点,若每次添加节点时,若令新节点有子节点,则令新节点为某个已被添加的节点的父亲,从而满足每个非叶节点至少有一个编号小于它的节点的限制。发现在此过程中仅需考虑已被添加的节点中,有多少节点可以新增父节点(即此时构成的森林中有多少树),有多少节点需要新增编号小于它的子节点。
于是考虑 DP,记 \(f_{i, j, k}\) 表示 由节点 \(i\sim n\) 构成的森林中,有 \(j\) 棵树,有 \(k\) 个节点需要新增编号小于它的子节点的方案数。初始化 \(f_{n + 1, 0, 0} = 1\),转移时考虑枚举当前要添加的节点 \(i\),然后枚举 \(i\) 的子节点的数量,讨论添加节点 \(i\) 后森林的形态:
-
若子节点数量为 0,则为如下两种情况:
- 孤立点 \(i\) 单独成为一棵树,此时有 \(j:=j+1\),\(k\) 不变。
- \(i\) 直接成为某个节点的子节点,此时有 \(j\) 不变,\(k:=k-1\)。
- 则有如下转移:
\[\begin{aligned} f_{i, j + 1, k} &\leftarrow f_{i - 1, j, k}\\ f_{i, j, k - 1} &\leftarrow k\times f_{i - 1, j, k} \end{aligned}\] -
若子节点数量为 1,则为如下两种情况:
- 直接成为某个节点的父节点,此时 \(j, k\) 不变。
- 成为某个节点的父节点的同时,成为某个另一棵树中节点的子节点,即将两棵树连接成为一棵树,此时 \(j:=j-1\),\(k:=k-1\)。
- 需要考虑成为左子节点还是右子节点,则有如下转移:
\[\begin{aligned} f_{i, j, k} &\leftarrow 2\times j\times f_{i + 1, j, k}\\ f_{i, j - 1, k - 1} &\leftarrow 2\times k\times (j - 1)\times f_{i + 1, j, k} \end{aligned}\] -
若子节点数量为 2,则为如下四种情况:
- 直接成为某个节点的父节点,则 \(j\) 不变,但是 \(k:=k + 1\)。
- 成为某个节点的父节点同时,成为某个另一棵树中节点的子节点,则 \(j:=j-1\),\(k\) 不变。
- 成为某两个不在同一棵树中节点的父节点,则 \(j:=j-1\),\(k\) 不变。
- 成为某两个不在同一棵树中节点的父节点的同时,成为某个另一棵树节点的子节点,则 \(j:=j-2\),\(k:=k-1\)。
- 考虑成为左子节点还是右子节点,则有如下转移:
\[\begin{aligned} f_{i, j, k + 1} &\leftarrow 2\times j\times f_{i + 1, j, k}\\ f_{i, j - 1, k} &\leftarrow 2\times k\times (j - 1)\times f_{i + 1, j, k}\\ f_{i, j - 1, k} &\leftarrow j\times (j - 1)\times f_{i + 1, j, k}\\ f_{i, j - 2, k - 1} &\leftarrow k\times (j - 1)\times (j - 2)\times f_{i + 1, j, k} \end{aligned}\]
按照上述思路画个图会更好理解,因为摸了所以不画了。上述转移时均限制了被连接的节点不在同一棵树中,则不会产生环,保证构造出来的都是合法的树。
保证最终形态是一棵有根树,且此时所有节点的子节点数量限制均已被满足(即所有节点均不需要再新增子节点),则答案即为 \(f_{1, 1, 0}\)。
实现时第一维可以滚动数组滚掉,状态为 \(O(n^2)\) 级别,转移为 \(O(n^3)\) 级别,则总时间复杂度 \(O(n^3)\) 级别。
//DP
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 310;
const LL p = 1e9 + 7;
//=============================================================
int n, l[kN], r[kN];
std::vector<std::vector <LL> > f(kN + 1, std::vector<LL> (kN + 1));
//=============================================================
void DP(std::vector<std::vector <LL> > &g, int lim, int i, int j) {
if (lim == 0) {
g[i + 1][j] += f[i][j], g[i + 1][j] %= p;
if (j) g[i][j - 1] += 1ll * j * f[i][j] % p, g[i][j - 1] %= p;
} else if (lim == 1) {
g[i][j] += 2ll * i * f[i][j] % p, g[i][j] %= p;
if (i && j) g[i - 1][j - 1] += 2ll * j * (i - 1) * f[i][j] % p, g[i - 1][j - 1] %= p;
} else {
if (j + 1 <= n) g[i][j + 1] += 2ll * i * f[i][j] % p, g[i][j + 1] %= p;
if (i) {
g[i - 1][j] += 2ll * j * (i - 1) * f[i][j] % p, g[i - 1][j] %= p;
g[i - 1][j] += 1ll * i * (i - 1) * f[i][j] % p, g[i - 1][j] %= p;
if (i >= 2 && j) g[i - 2][j - 1] += 1ll * j * (i - 1) * (i - 2) * f[i][j] % p, g[i - 2][j - 1] %= p;
}
}
}
//=============================================================
int main() {
// freopen("1.txt", "r", stdin);
std::ios::sync_with_stdio(0), std::cin.tie(0);
std::cin >> n;
for (int i = 1; i <= n; ++ i) std::cin >> l[i] >> r[i];
f[0][0] = 1;
for (int u = n; u; -- u) {
std::vector<std::vector <LL> > g(kN + 1, std::vector<LL> (kN + 1));
for (int lim = l[u]; lim <= r[u]; ++ lim) {
for (int i = 0; i <= n; ++ i) {
for (int j = 0; j <= n; ++ j) {
DP(g, lim, i, j);
}
}
}
std::swap(f, g);
}
std::cout << f[1][0] << "\n";
return 0;
}
写在最后
学到了什么:
- J:考虑不合法元素满足的的偏序关系,观察能否通过数学式进行表示并检查。
- D:根据题目性质减去不合法状态。
- C:需要达到某种目标状态,有多种操作且直接操作过于复杂的莫名奇妙最优化问题,尝试图论转化。
- H:大力随机呃呃
- G:若有编号大小限制,考虑按照有序状态添加元素,并考虑通过调整使之满足限制。
这场题都挺有意思的,不愧是霓虹人!