做题记录 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\)),子树内排列方案数。转移:

\[f_{v_1,c_1,s_1}f_{v_2,c_2,s_2}\binom{c_1+c_2}{c_1}\binom{sz_{v_1}+sz_{v_2}-c_1-c_2}{sz_{v_1}-c_1}\rightarrow f_{v,c_1+c_2,s_1+s_2} \]

值得一提的是,树上二维背包复杂度 \(\mathcal{O}(n^4)\),满二叉树可以卡满。

如果先转移了 \(u\),则其权重就不确定了,因此需要先贡献答案再转移 \(u\)。同时,\(s=0\) 的转移也要特殊处理,因为只要 \(u\) 权重 \(>p\),就有 \(s=0\)。

转移答案:

\[f_{v,c,s}\rightarrow ans_{u,c+1} \]

\(s=0\):

\[f_{u,c,0}=(sz_u-c)\cdot(sz_u-1)! \]

这两个转移都不重要,主要看背包。发现 \(s\) 是简单卷积,考虑插值,于是这部分 \(\mathcal{O}(n^3)\)。考虑求答案,对于一个多项式 \(f(x)\),其系数向量为 \(\bm{u}\),点值向量为 \(\bm{v}\),则可根据 Lagrange 插值构造矩阵 \(A\),使得

\[\bm{u}=A\bm{v} \]

我们要求:

\[\bm{g}^T\bm{u}=\bm{g}^TA\bm{v} \]

预处理出 \(\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\) 有关,做半在线差卷积。

posted @ 2026-07-06 00:09  SmpaelFx  阅读(18)  评论(0)    收藏  举报