巨神兵 题解

news/2025/3/17 13:28:29/文章来源:https://www.cnblogs.com/XuYueming/p/18759987

题意简述

给你 \(n\) 个点 \(m\) 条边的有向图,请问在 \(2^m\) 种选择边的方式中,有多少种方式不会成环?对 \(10^9+7\) 取模。

\(n\leq17,m\leq n\cdot(n-1)\)

题目分析

看到数据范围这么小,应该是状压了。我们计数对象是边,状态记录的却是某些点怎么样了,怎么设计状态并转移呢?

考虑怎么刻画一个 DAG。我们判断 DAG 使用拓扑排序,而它正是一种对于点的过程,那么尝试从这里下手。它将我们的点分成了若干组,第一组是入度为 \(0\) 的点构成的集合,第二组是去掉第一组和所连的边后入度为 \(0\) 的点,以此类推。那我们就考虑一组一组地加点。

不妨我们考虑往第一组之前加一组点,即新加的一组点中,连出若干条边至原先的第一组点。往最后一组之后再加一组,和在第一组之前再加一组是相同的:考虑将边反向,成环状态不变,而此时第一组变成了最后一组,最后一组变成了第一组。

我们设 \(f_{S,T}\) 表示点集为 \(S\) 的子图,其中第一组是 \(T\) 的方案数。只有 \(T \subseteq S\) 的状态才是有效的,事实上,三进制 DP 是可行的,如果愿意写的话。初始值 \(f_{S,S}=1\)。答案即为 \(\sum\limits_{T}f_{\{i\}_{i=1}^n,T}\)

转移的时候,枚举新的第一层 \(T'\)\(T'\cap S=\varnothing\)。将 \(T'\) 连出的边分为连向 \(T\) 和连向 \(S\setminus T\) 讨论。对于一个 \(u\in T\),它必须要至少有一个入度,假设有 \(c_1\)\(T'\) 中的点连向它,那么方案数为 \(2^{c_1}-1\);对于一个 \(u\in T'\),连向 \(S\setminus T\) 的边数设为 \(c_2\),那么随便连,方案数为 \(2^{c_2}\)

于是我们得到了 \(\mathcal{O}(n\cdot4^n + m)\) 的做法,考虑优化。尝试将状态简化为 \(\mathcal{O}(2^n)\),记 \(f_S\) 表示点集为 \(S\) 的方案数。

类似枚举新的第一层 \(T\) 满足 \(T\cap S=\varnothing\),再记 \(c_1\) 表示 \(T\) 中的点连向 \(S\) 中的点的边数。可能我们会列出一个一看就不对的转移 \(f_{S\cup T}\Leftarrow f_S\cdot 2^{c_1}\)(其中 \(a\Leftarrow b\) 表示 \(a\gets a+b\))。这样并不能保证 \(S\) 中的第一层都会有入度,从而导致了 \(S\cup T\) 的第一层为 \(T\) 并上原先 \(S\) 中第一层的一部分。这显然会导致重复,对于某一个局面,它的第一组会被多种加点方式统计到。我们接下来要把这些重复容斥掉。

注意到,容斥系数为 \((-1)^{|T|+1}\),即 \(f_{S\cup T}\Leftarrow (-1)^{|T|+1}\cdot2^{c_1}\cdot f_S\) 即为正确的转移。

正确性证明:

\(w_{T\Rightarrow S}\) 表示 \(T\) 中的点连向 \(S\) 中的点的边数。设 \(g_{S,T}\) 表示点集为 \(S\) 的子图,其中第一组是 \(T\) 的方案数。再设 \(h_{S,T}\) 表示点集为 \(S\) 的子图,其中第一组至少为 \(T\) 的方案数。

根据容斥,我们有 \(g_{S,T}=\sum\limits_{T\subseteq T'\subseteq S}(-1)^{|T'|-|T|}h_{S,T'}\)。而 \(h_{S,T} = f_{S\setminus T}\cdot 2^{w_{T\Rightarrow S\setminus T}}\)(考虑到 \(f\) 中囊括了第一组 \(\subseteq S\setminus T\) 的所有情况,连边后的第一组 \(T'\) 囊括了 \(T\subseteq T'\subseteq S\) 的所有情况)。所以:

\[\begin{aligned} f_S &= \sum_{\varnothing\subset T\subseteq S} g_{S,T} \\&= \sum_{\varnothing\subset T\subseteq S} \sum\limits_{T\subseteq T'\subseteq S}(-1)^{|T'|-|T|}h_{S,T'} \\&= \sum_{\varnothing\subset T'\subseteq S} \sum\limits_{\varnothing\subset T\subseteq T'}(-1)^{|T'|-|T|}h_{S,T'} \\&= \sum_{\varnothing\subset T'\subseteq S} \sum\limits_{\varnothing\subset T\subseteq T'}(-1)^{|T'|-|T|}f_{S\setminus T'}\cdot 2^{w_{T'\Rightarrow S\setminus T'}} \\&= \sum_{\varnothing\subset T'\subseteq S} (-1)^{|T'|}\cdot 2^{w_{T'\Rightarrow S\setminus T'}}\cdot f_{S\setminus T'} \sum\limits_{\varnothing\subset T\subseteq T'}(-1)^{|T|} \\&= \sum_{\varnothing\subset T'\subseteq S} (-1)^{|T'|}\cdot 2^{w_{T'\Rightarrow S\setminus T'}}\cdot f_{S\setminus T'} \sum\limits_{i=1}^{|T'|}\binom{|T'|}{i}1^{|T'|-i}(-1)^{i} \\&= \sum_{\varnothing\subset T'\subseteq S} (-1)^{|T'|}\cdot 2^{w_{T'\Rightarrow S\setminus T'}}\cdot f_{S\setminus T'} \Bigg(\sum\limits_{i=0}^{|T'|}\binom{|T'|}{i}1^{|T'|-i}(-1)^{i}-1\Bigg) \\&= \sum_{\varnothing\subset T'\subseteq S} (-1)^{|T'|}\cdot 2^{w_{T'\Rightarrow S\setminus T'}}\cdot f_{S\setminus T'} \Big([T'=\varnothing]-1\Big) \\&= \sum_{\varnothing\subset T'\subseteq S} (-1)^{|T'|+1}\cdot 2^{w_{T'\Rightarrow S\setminus T'}}\cdot f_{S\setminus T'} \\&= \sum_{T\subset S} (-1)^{|S\setminus T|+1}\cdot 2^{w_{S\setminus T\Rightarrow T}}\cdot f_{T} \\ \end{aligned} \]

发现这就是我们的转移方程,而容斥系数即为 \((-1)^{|S\setminus T|+1}\)

\(\mathcal{O}(3^n)\) 枚举 \(S\)\(T\),再 \(\mathcal{O}(n)\) 地计算 \(w_{T\Rightarrow S}\) 就能做到 \(\mathcal{O}(n\cdot 3^n + m)\)。发现在 \(S\) 固定时,枚举 \(T\) 的同时,可以轻松递推维护 \(w\)。对于从小到大枚举的 \(T\),取一 \(x\in T\)\(w_{T\setminus\{x\}\Rightarrow S}\) 已经计算得出,那么加上 \(w_{\{x\}\Rightarrow S}\) 就是 \(w_{T\Rightarrow S}\)。于是可以做到 \(\mathcal{O}(3^n + m)\)

代码

$\mathcal{O}(n\cdot4^n + m)$
#include <cstdio>
#include <iostream>
#include <limits>
#include <cassert>
using namespace std;
using namespace Mod_Int_Class;const int N = 13, M = 1 << 10 | 520;int n, m, e[N], r[N];
mint f[M][M], pw[N * N];signed main() {
#ifndef XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%d%d", &n, &m);pw[0] = 1;for (int i = 1; i <= m; ++i)pw[i] = pw[i - 1] + pw[i - 1];for (int u, v, i = 1; i <= m; ++i) {scanf("%d%d", &u, &v);e[u - 1] |= 1 << (v - 1);r[v - 1] |= 1 << (u - 1);}for (int s = 0; s < 1 << n; ++s)f[s][s] = 1;for (int s = 0; s < 1 << n; ++s) {for (int t = s; t; t = (t - 1) & s) {int S = ((1 << n) - 1) ^ s;for (int ss = S; ss; ss = (ss - 1) & S) {mint res = 1;for (int i = 0; i < n; ++i)if (t & 1 << i) {int x1 = __builtin_popcount(r[i] & ss);res *= pw[x1] - 1;}for (int i = 0; i < n; ++i)if (ss & 1 << i) {int x2 = __builtin_popcount(e[i] & (s ^ t));res *= pw[x2];}f[s | ss][ss] += f[s][t] * res;}}}mint ans = 0;for (int s = 0; s < 1 << n; ++s)ans += f[(1 << n) - 1][s];printf("%d", ans.raw());return 0;
}
$\mathcal{O}(n\cdot 3^n + m)$
#include <cstdio>
#include <iostream>
#include <limits>
#include <cassert>
using namespace std;
using namespace Mod_Int_Class;const int N = 18;int n, m, e[N];mint f[1 << N | 10], pw[N * N];signed main() {
#ifndef XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%d%d", &n, &m);pw[0] = 1;for (int i = 1; i <= m; ++i)pw[i] = pw[i - 1] + pw[i - 1];for (int u, v; m--; )scanf("%d%d", &u, &v), e[u - 1] |= 1 << (v - 1);f[0] = 1;for (int st = 0; st < 1 << n; ++st) {int S = ((1 << n) - 1) ^ st;for (int T = S; T; T = (T - 1) & S) {int cnt = 0;for (int i = 0; i < n; ++i)if (T & 1 << i)cnt += __builtin_popcount(e[i] & st);if (__builtin_popcount(T) & 1)f[st | T] += f[st] * pw[cnt];elsef[st | T] -= f[st] * pw[cnt];}}printf("%d", f[(1 << n) - 1].raw());return 0;
}
$\mathcal{O}(3^n + m)$
#include <cstdio>
#include <iostream>
#include <limits>
#include <cassert>
using namespace std;
using namespace Mod_Int_Class;const int N = 18;int n, m, e[1 << N | 10];mint f[1 << N | 10], pw[N * N];
int ccc[1 << N | 10], popc[1 << N | 10];signed main() {
#ifndef XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%d%d", &n, &m);pw[0] = 1;for (int i = 1; i <= m; ++i)pw[i] = pw[i - 1] + pw[i - 1];for (int s = 1; s < 1 << n; ++s)popc[s] = popc[s >> 1] + (s & 1);for (int u, v, i = 1; i <= m; ++i)scanf("%d%d", &u, &v), e[1 << (u - 1)] |= 1 << (v - 1);f[0] = 1;for (int st = 0; st < 1 << n; ++st) {for (int T = (st + 1) | st; T < 1 << n; T = (T + 1) | st) {int S = T ^ st;ccc[S] = ccc[S & (S - 1)] + popc[e[S & -S] & st];if (popc[S] & 1)f[T] += f[st] * pw[ccc[S]];elsef[T] -= f[st] * pw[ccc[S]];}}printf("%d", f[(1 << n) - 1].raw());return 0;
}
卡常后
#include <cstdio>const int mod = 1e9 + 7;
const int N = 18, M = 1 << 17 | 520;int n, m, e[M];
int f[M], pw[N * N];
int ccc[M], popc[M];signed main() {
#ifndef XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%d%d", &n, &m);pw[0] = 1;for (int i = 1, lst = 1; i <= m; ++i) {lst <<= 1;lst = lst >= mod ? lst - mod : lst;pw[i] = lst;}int U = (1 << n) - 1;for (int s = 1; s <= U; ++s)popc[s] = popc[s >> 1] + (s & 1);for (int u, v, i = 1; i <= m; ++i)scanf("%d%d", &u, &v), e[1 << (u - 1)] |= 1 << (v - 1);f[0] = 1;for (int st = 0; st < U; ++st) {for (int T = (st + 1) | st; T <= U; T = (T + 1) | st) {int S = T ^ st;int x = ccc[S & (S - 1)] + popc[e[S & -S] & st];ccc[S] = x;if (popc[S] & 1) {f[T] += 1ll * f[st] * pw[x] % mod;f[T] = f[T] >= mod ? f[T] - mod : f[T];} else {f[T] -= 1ll * f[st] * pw[x] % mod;f[T] = f[T] < 0 ? f[T] + mod : f[T];}}}printf("%d", f[U]);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/900350.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

wr30u 救砖

变砖了 给 wr30u 刷成了 at3000 的uboot,进而导致我的wr30u路由器成了转,进不去uboot了! 准备硬件 usb转串口设备 购买 usb转串口(ttl)设备,在淘宝随便买就行 没啥差别,几块钱一个:目前流行ch340系列的,推荐 ch340g 即可, 功能更强大也贵杜邦线 一般 你在购买 usb转串口…

滑动窗口-2962.统计最大元素出现至少k次的子数组

设 mx=max(nums)。 右端点 right 从左到右遍历 nums。遍历到元素 x=nums[right] 时,如果 x=mx,就把计数器 cntMx 加一。 如果此时 cntMx=k,则不断右移左指针 left,直到窗口内的 mx 的出现次数小于 k 为止。此时,对于右端点为 right 且左端点小于 left 的子数组,mx 的出现…

The Hackers Labs (防御靶场练习)

我打蓝队时我们队的靶机be like:The Hackers Labs (防御靶场练习) Binary Trail(二进制痕迹) 对公司基础设施至关重要的 Linux 服务器已显示出可疑活动的迹象。在例行审计期间,已发现位于非标准目录中的未知二进制文件的存在。此文件的性质和来源尚不确定,但其行为表明可…

Linux 安装配置Anaconda

下载地址 https://www.anaconda.com/download/success 选择系统版本,复制链接 wget https://repo.anaconda.com/archive/Anaconda3-2024.10-1-Linux-x86_64.sh脚本赋权,再执行安装 chmod +x Anaconda3-2024.10-1-Linux-x86_64.sh./Anaconda3-2024.10-1-Linux-x86_64.sh然后出…

Linux 下载安装CUDA Toolkit 12.8,配置Nvidia Driver

cuda下载地址 https://developer.nvidia.com/cuda-downloadsnvidia-smi Mon Mar 17 02:08:35 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 570.124.06 Driver Version: 570.124.06 CUDA …

在 Pycharm 中调试 Django 代码片段

在Pycharm中可以给代码打断点,查看变量信息等。 Django 调试代码可以运行起来打日志,但无法中断代码调试。 Django 提供了一个 manage.py shell 命令可以创建 Django 环境,手工执行代码调试。 本文介绍如何把manage shell 和 Pycharm结合起来,在不启动 Django 的情况下调试…

四则运算系统

这个作业属于哪个课程 软件工程2班这个作业要求在哪里 结对项目这个作业的目标 团队共同实现一个自动生成小学四则运算题目的命令行程序合作人员朱雅子 3223004823 黄海怡 3223004296项目地址Github链接 一、PSP表格PSP2.1 Personal Software Process Stages 预估耗时(分钟) …

【每日一题】20250317

小时候以为人生是踏歌而行,后来才明白其实脚下踏着的是利刃和刀锋。【每日一题】(不定项)已知曲线 \(C_1\):\(y=\cos x\),\(C_2\):\(\displaystyle y=\sin(2x+\frac{2\pi}{3})\). (1) 下列结论正确的是 A. 把 \(C_1\) 上各点的横坐标伸长到原来的 \(2\) 倍,纵坐标不变,…

asyncio协程

目录使用协程重要的几个函数使用异步方法爬取Microsoft Bing图片asyncio.Queue(maxsize=) 说明1: 正常的程序都是从上到下依次执行的,如果遇到了要等待的地方,就会阻塞,等待相应的代码执行完毕后,再往下执行。 说明2: 协程(Coroutine) 是一种特殊的函数,它可以在执行过…

生产管理到底是啥?生产计划又该怎么做?

你是不是经常听到“生产管理”这四个字,但总觉得它挺虚的? 其实,生产管理说白了,就是为了让工厂或企业的生产顺利进行,保证产品按时、高质量、低成本地交付。这里面涉及到人、机器、原材料、时间、成本等各种因素,怎么把这些东西合理安排,就是生产管理的核心。 而生产计…

【日记】带着半边脸的眼影跑了小半个新区县城……(2302 字)

正文这个周末,充实到仿佛跟工作日的自己像是两个人。事情一件件来说吧。周五太过遥远,记不清了;周六几乎全天都在研究化妆;周天又几乎全在跳舞。第一件事,研究化妆。周五的晚上,基本上所有的化妆品就都到了。拆了好多好多。周六上午就试了一下所有的化妆品。不得不说,男…

AMBA总线学习(二)---AHB-Lite

AHB-Lite的硬件架构可以分为四部分,分别是Master、Slave、Decoder、MUX。 Maeter信号:Name Destination DescriptionHADDR[31:0] slave和decoder 32bit的地址总线(不是严格限制为32bit)HBURST[2:0] slave 突发传输类型HMASTERLOCK slave 用来实现原子操作的HPROT[3:0] slav…