[JZOJ5739]毒奶 子集DP

本文介绍了一种O(3^n*n)复杂度的暴力解法,通过点的压缩和子集动态规划解决了一个特定的图论问题。问题转化为给定若干黑白点及边,求如何添加更多边使图联通的所有可能方案的数量。

这里讲的是O(3nn)O(3n⋅n)的可以过的暴力,正解不会。。。
把问题转化成给了nn个白点,n个黑点,给定了n1n−1条白白边和黑黑边,求再填nn条黑白边使之联通的方案数。
先用给定的边缩点,记下缩点后每个大点的size,然后随意钦定一个白点当根,剩下就是要求一层黑一层白的填,直接子集DP即可。
具体地,设FS,c,iFS,c,i表示当前已经选了SS中的点,当前层颜色是c,伸向下一层的边有ii条。只要枚举S的一个子集TT,且T中元素颜色全为cc,记T中元素个数是cntcntsizesize的和是tottot,那么就有转移:

FS,c,totcntFST,!c,cntcnt!FS,c,tot−cnt←FS−T,!c,cnt∗cnt!

最后答案还要乘上irootsizei∏i≠rootsizei,因为一个大点中每个点都可以作为连接上一层父亲的点。
还要减减枝,比如说一种颜色已经选完之后,下一层要立即选择全集什么的。。。就可以水过了。

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 45
#define ll long long
#define up(x,y) (x=(x+(y))%mod)
using namespace std;
const int mod=998244353;
int n,m,p[N],fa[N],e[N][2],sz[N],pc[1050000],tot[1050000];
ll f[1050000][2][21],ans,fac[N];
int getfa(int v)
{
    if(fa[v]==v) return v;
    return (fa[v]=getfa(fa[v]));
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
        scanf("%d%d",&e[i][0],&e[i][1]);
    for(int i=m+1;i<n;i++)
    {
        scanf("%d%d",&e[i][0],&e[i][1]);
        e[i][0]+=n;e[i][1]+=n;
    }
    fac[0]=1;
    for(int i=1;i<=n;i++)
        fac[i]=fac[i-1]*i%mod;
    for(int i=1;i<=(n<<1);i++)
        fa[i]=i;    
    for(int i=1;i<n;i++)
        if(getfa(e[i][0])!=getfa(e[i][1])) fa[fa[e[i][0]]]=fa[e[i][1]];
    for(int i=1;i<=(n<<1);i++)
        sz[getfa(i)]++;
    int cnt=0;  
    for(int i=1;i<=n;i++)
        if(getfa(i)==i) sz[++cnt]=sz[i];
    m=cnt;
    for(int i=n+1;i<=(n<<1);i++)
        if(getfa(i)==i) sz[++cnt]=sz[i];
    n=cnt;      

    f[0][1][sz[n]]=1;n--;   
    int R[2];R[0]=((1<<m)-1),R[1]=R[0]^((1<<n)-1);  
    for(int s=0;s<(1<<n);s++)
        for(int i=1;i<=n;i++)
            if((s>>(i-1))&1) pc[s]++,tot[s]+=sz[i];
    for(int s=0;s<(1<<n);s++)
    {
        int u[2];u[0]=s&R[0],u[1]=s&R[1];
        for(int c=0;c<=1;c++)
        {
            if(u[c^1]==R[c^1]&&s+1!=(1<<n)) continue;
            for(int t=u[c];t;t=(t-1)&u[c])
                if(f[s^t][c^1][pc[t]])
                    up(f[s][c][tot[t]-pc[t]],f[s^t][c^1][pc[t]]*fac[pc[t]]);
        }
    }
    for(int c=0;c<=1;c++)
        up(ans,f[(1<<n)-1][c][0]);
    for(int i=1;i<=n;i++)
        ans=ans*sz[i]%mod;      
    printf("%lld",ans);
    return 0;
}
### 子集问题与动态规划算法实现 #### 动态规划的核心思想 动态规划是一种通过分解问题为更小的子问题来求解复杂问题的方法。这种方法特别适用于那些具有重叠子问题和最优子结构性质的问题[^1]。在处理子集和问题时,动态规划可以通过构建一张二维表格 `dp` 来存储中间状态的结果。 #### 子集和问题描述 给定一个正整数集合 \( S = \{x_1, x_2, ..., x_n\} \) 和目标值 \( c \),我们需要找到是否存在 \( S \) 的某个子集,其元素之和等于 \( c \)[^3]。此问题通常被称为子集和问题 (Subset Sum Problem)。 #### 动态规划的状态定义 设 `dp[i][j]` 表示前 \( i \) 个元素能否构成和为 \( j \) 的子集。初始条件为当 \( j=0 \) 时,任何数量的元素都可以组成和为零的情况(即空集)。因此有: \[ dp[0][0] = True \] 其他情况下,默认初始化为 False: \[ dp[i][j] = False \text{(for all } i,j>0)\] 转移方程如下: - 如果当前元素 \( x_i \leq j \), 则可以选择加入或者不加入该元素: \[ dp[i][j] = dp[i-1][j] \lor dp[i-1][j-x_i] \] - 否则只能选择不加入该元素: \[ dp[i][j] = dp[i-1][j] \] 最终答案就是查看 `dp[n][c]` 是否为真[^5]。 以下是基于上述逻辑的一种Python实现方式: ```python def subset_sum_dp(nums, target): n = len(nums) # 创建 DP 数组并初始化 dp = [[False]*(target+1) for _ in range(n+1)] # 当目标值为 0 时,总是可以由空集满足 for i in range(n+1): dp[i][0] = True # 填充 DP 表格 for i in range(1, n+1): for j in range(1, target+1): if nums[i-1] <= j: dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]] else: dp[i][j] = dp[i-1][j] return dp[n][target] # 测试数据 numbers = [3, 34, 4, 12, 5, 2] goal = 9 print(subset_sum_dp(numbers, goal)) # 输出应为True 或者 False ``` 以上代码实现了使用动态规划解决子集和问题的过程[^2]。 #### 性能分析 相比于简单的递归方法,这种动态规划解决方案的时间复杂度降低到了 O(nc),其中 n 是输入列表长度,而 c 是目标数值大小。尽管如此,在某些极端条件下仍可能存在效率瓶颈,比如非常大的目标值或过长的序列[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值