做题记录 2026.07
2026.07
代码能力严重下降。
*QOJ15303 Basic Counting Practice Problems
这题太厉害了。
不难想到只考虑子树内的排列,对要求的 \((i,j)\),设 \(i\) 子树内其相对位置为 \(c\)(有 \(c\) 个点权重 \(\le p_i\)),最后乘系数 \(\binom{j-1}{c-1}\binom{n-j}{sz_i-c}(n-sz_i)!\) 即可。考虑一个有道理的 DP,设 \(f_{u,c,s}\) 表示在 \(u\) 子树内排列 \(1\sim sz_u\),\(u\) 所在的权重 \(\le c\) 的连通块大小为 \(s\)(若 \(u\) 权重 \(>c\),则为 \(0\)),子树内排列方案数。转移:
值得一提的是,树上二维背包复杂度 \(\mathcal{O}(n^4)\),满二叉树可以卡满。
如果先转移了 \(u\),则其权重就不确定了,因此需要先贡献答案再转移 \(u\)。同时,\(s=0\) 的转移也要特殊处理,因为只要 \(u\) 权重 \(>p\),就有 \(s=0\)。
转移答案:
\(s=0\):
这两个转移都不重要,主要看背包。发现 \(s\) 是简单卷积,考虑插值,于是这部分 \(\mathcal{O}(n^3)\)。考虑求答案,对于一个多项式 \(f(x)\),其系数向量为 \(\bm{u}\),点值向量为 \(\bm{v}\),则可根据 Lagrange 插值构造矩阵 \(A\),使得
我们要求:
预处理出 \(\bm{g}^TA\),这样就可以对每个 \((i,j)\) \(\mathcal{O}(n)\) 计算答案,总复杂度 \(\mathcal{O}(n^3)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int MOD=1'000'000'007;
void add(int &x,int y){
x+=y;
if(x>=MOD) x-=MOD;
}
int add2(int x,int y){
x+=y;
if(x>=MOD) x-=MOD;
return x;
}
int qpow(int a,ll b){
int mul=1;
while(b){
if(b&1) mul=(ll)mul*a%MOD;
a=(ll)a*a%MOD;
b>>=1;
}
return mul;
}
const int N=705;
int fact[N<<1],invfact[N<<1];
int n,q[N],sz[N],w[N],ans[N][N];
vector<int> G[N],f[N][N];
int C(int n,int m){
if(n<m) return 0;
return (ll)fact[n]*invfact[m]%MOD*invfact[n-m]%MOD;
}
void dfs(int u,int fa){
sz[u]=0;
for(int x=0;x<=n;x++) f[u][x]={1};
bool flag=true;
for(auto v:G[u]){
if(v==fa) continue;
dfs(v,u);
for(int x=0;x<=n;x++){
vector<int> tmp(sz[u]+sz[v]+1);
for(int i=0;i<=sz[u];i++) for(int j=0;j<=sz[v];j++)
add(tmp[i+j],(ll)C(i+j,i)*C(sz[u]+sz[v]-i-j,sz[u]-i)%MOD*f[u][x][i]%MOD*f[v][x][j]%MOD);
f[u][x].swap(tmp);
}
sz[u]+=sz[v];
}
for(int x=0;x<=n;x++) for(int p=0;p<=sz[u];p++) add(ans[u][p+1],(ll)w[x]*f[u][x][p]%MOD*x%MOD);
// s=0
for(int x=0;x<=n;x++){
vector<int> tmp(sz[u]+2);
for(int j=0;j<=sz[u];j++) add(tmp[j+1],(ll)(j+1)*x%MOD*f[u][x][j]%MOD);
f[u][x].swap(tmp);
}
sz[u]++;
for(int p=0;p<=sz[u];p++){
int k=(ll)(sz[u]-p)*fact[sz[u]-1]%MOD;
for(int x=0;x<=n;x++) add(f[u][x][p],k);
}
}
void init(){
fact[0]=1;
for(int i=1;i<(N<<1);i++) fact[i]=(ll)i*fact[i-1]%MOD;
invfact[(N<<1)-1]=qpow(fact[(N<<1)-1],MOD-2);
for(int i=(N<<1)-1;i>0;i--) invfact[i-1]=(ll)i*invfact[i]%MOD;
vector<int> f{1};
for(int i=0;i<=n;i++){
f.push_back(0);
for(int j=f.size()-1;j>0;j--) f[j]=add2(f[j-1],MOD-(ll)f[j]*i%MOD);
f[0]=(ll)f[0]*(MOD-i)%MOD;
}
for(int i=0;i<=n;i++){
for(int j=f.size()-1;j>0;j--) add(f[j-1],(ll)i*f[j]%MOD);
for(int j=0;j+1<f.size();j++) f[j]=f[j+1];
f.pop_back();
int k=(ll)invfact[i]*invfact[n-i]%MOD;
if((n-i)&1) k=MOD-k;
int sum=0;
for(int j=0;j<=n;j++) add(sum,(ll)q[j]*k%MOD*f[j]%MOD);
w[i]=sum;
f.push_back(0);
for(int j=f.size()-1;j>0;j--) f[j]=add2(f[j-1],MOD-(ll)f[j]*i%MOD);
f[0]=(ll)f[0]*(MOD-i)%MOD;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&q[i]);
init();
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
G[u].push_back(v),G[v].push_back(u);
}
dfs(1,1);
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
int sum=0;
for(int k=1;k<=j&&k<=sz[i];k++) add(sum,(ll)C(j-1,k-1)*C(n-j,sz[i]-k)%MOD*fact[n-sz[i]]%MOD*ans[i][k]%MOD);
printf("%d ",sum);
}
printf("\n");
}
return 0;
}
*P15568 [COCI 2025/2026 #5] 摆放 / Slaganje
两个方向,删点转为子问题、构造一个排列使得每种长度的边都至少出现一次然后轮换一圈。对于删点,发现由于是对边考虑,不能转为纯粹的子问题,没有前途。于是尝试构造排列使得 每种长度的边都出现至少一次。我的想法是充分利用每条边,然后按从小往大填入,但这样限制太多,做不了。
看题解发现,其实我们只需要 \(m=\left\lfloor\frac{n}{2}\right\rfloor\) 条边(长度分别是 \(1\sim m\)),并且发现菊花的性质很好,于是尝试把图拆成若干互不相交的菊花。奇偶分层,以偶数/奇数层为根拉出菊花,发现总有一种总边数 \(\ge m\) 的方案(这是 经典结论 了)。
然后 从大往小 填边,具体地,从一个边数 \(\ge 2\) 的开始(只要根度数 \(\ge 2\) 就一定有),在左侧放长 \(m,m-1,\dots\) 的边,当前菊花放完了就在另一边放下一个菊花,不断操作。
这样构造出来一定满足要求,设左边能放下的最长边长度 \(a\),右边是 \(b\),放完第一个菊花后 \(a\le m-3,\ b=n-m-2\ge m-2\),对于之后的每个菊花,不妨设其在左边放置,最后一条边长 \(x\),则上一个菊花最后一条边 \(\ge x+1\),则 \(b\ge x-1\),所以可以连续放下边。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<int,ll> pil;
typedef pair<ll,int> pli;
typedef pair<ll,ll> pll;
template<typename T>
void chkmin(T &x,const T &y){x=min(x,y);}
template<typename T>
void chkmax(T &x,const T &y){x=max(x,y);}
const int inf=0x3f3f3f3f;
const ll infll=0x3f3f3f3f3f3f3f3f;
const int N=3005;
int n,fa[N],dep[N],p[N],vis[N];
vector<int> G[N];
void dfs(int u,int f){
fa[u]=f,dep[u]=dep[f]+1;
for(auto v:G[u]){
if(v==f) continue;
dfs(v,u);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
G[u].push_back(v),G[v].push_back(u);
}
int r;
for(int i=1;i<=n;i++) if(G[i].size()>=2){
r=i;
break;
}
dfs(r,r);
int cnt=0;
for(int i=1;i<=n;i++) if(dep[i]&1) cnt+=G[i].size()-(i!=r);
vector<int> star;
if(cnt>=(n>>1)){
for(int i=1;i<=n;i++) if(dep[i]&1) star.push_back(i);
}
else{
for(int i=1;i<=n;i++) if(!(dep[i]&1)) star.push_back(i);
}
sort(star.begin(),star.end(),[&](int x,int y){return G[x].size()-(x!=r)>G[y].size()-(y!=r);});
// printf("star ");for(auto i:star) printf("%d ",i);printf("\n");
for(int i=(n>>1),j=0,k=0,l=0,r=n+1,t=1;i;i--,k++){
bool flag=(i==(n>>1));
while(j<star.size()){
if(k>=G[star[j]].size()) j++,k=0,flag=true;
if(G[star[j]][k]==fa[star[j]]) k++;
else break;
}
if(flag){
t^=1;
if(!t) l++;
else r--;
}
// printf("i=%d j=%d k=%d l=%d r=%d t=%d\n",i,j,k,l,r,t);
if(!t) p[l]=star[j],p[l+i]=G[star[j]][k];
else p[r]=star[j],p[r-i]=G[star[j]][k];
// for(int j=1;j<=n;j++) printf("%d ",p[j]);printf("\n---\n");
}
for(int i=1;i<=n;i++) vis[p[i]]=1;
vector<int> v;
for(int i=1;i<=n;i++) if(!vis[i]) v.push_back(i);
for(int i=1,j=0;i<=n;i++) if(!p[i]) p[i]=v[j++];
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++) printf("%d ",p[(j+i)%n+1]);
printf("\n");
}
return 0;
}
*P9330 [JOIST 2023] JOI 国的节日 2 / Festivals in JOI Kingdom 2
一眼看上去有点像 NOIP2025 t2,但完全不一样,笔者在类似的思路上飞奔然后寄了。
改进的算法是按右端点排序从左往右贪心(最开始想的是对称的,即左端点排序从右往左,但没啥前途,不难想到对称回来)。
分别观察两个解的形态,然后忘了选出的区间不交,然后不会了看题解。
给原算法选出的区间打 A 标签,给改进算法选出的区间打 B 标签,没有则为 O,都有则为 AB(A 区间、B 区间集合包含这种)。
意识到区间不交。对于原算法,不存在 非A 区间左端点在两个相邻 A 区间的缝隙(前一个的右端点 ~ 后一个的左端点)之间。对于改进算法,不存在 非B 区间在相邻 B 区间右端点组成的范围内部。
钦定 A 区间、B 区间总数相等,发现 第 \(i-1\) 个 A 区间右端点 \(\le\) 第 \(i\) 个 B 区间右端点 \(\le\) 第 \(i\) 个 A 区间右端点。第一个不等号,如果不满足,把前 \(i\) 个 A 和 \(i-1\) 之后的 B 拼起来答案变大。第二个不等号,感性理解一下,改进算法选的是右端点尽量小的。
妙妙 DP 环节,核心思想是 记录相对位置,后面的区间是 插入 而非 填空。
最初的想法是,从左往右,每次加入一对 A,B,加入右端点在当前 B 右端点到上一个 B 右端点之间的 O,发现需要多在状态中记录 O 左端点能插入的位置,状态 \(\mathcal{O}(n^2)\),转移 \(\mathcal{O}(n)\)。
发现左端点限制更多,对左端点做,于是从右往左,每次加入 A,B,加入左端点在当前 B 右端点到上一个 B 右端点间的 O,发现右端点只要大于上一个 B 的右端点就行了,状态 \(\mathcal{O}(n)\),转移 \(\mathcal{O}(n)\)。
列一下转移式子,发现从 \(i\) 转移到 \(j\) 系数只与 \(i,j,i+j\) 有关,做半在线差卷积。

浙公网安备 33010602011771号