这道题第一个结论都不知道怎么拿部分分啊


题意

一个 \(n\times n\) 的方阵 \(M\),上面除了 \(k\) 个特殊位置,其他位置都是 \(1\)。第 \(i\) 个特殊位置在 \((x_i,y_i)\),值为 \(v_i\)。

求 \(M\) 的积和式,即求对所有 \(1\sim n\) 的排列 \(P=(p_1,p_2,\dots,p_n)\) 下式的和:

\[\prod_{i=1}^nM_{i,p_i}
\]

其实就是行列式去掉 \(\pm 1\) 的系数。

数据规模:\(n\le10^6,k\le50\)。


解析

初步分析

积和式有一个比较易懂的公式,虽然不是很容易想到。记 \(\mathrm{perm}(M)\) 为 \(M\) 的积和式,方阵的加法定义为对应位置相加;对大小为 \(n\) 的方阵 \(M\) 以及全集 \(\{1,2,\dots,n\}\) 的子集 \(S,T\),简记 \(M_{S,T}\) 表示按相对位置取出 \(M_{i,j}\)(\(i\in S,j\in T\))的方阵,则

\[\mathrm{perm}(A+B)=\sum_{S\subseteq\{1,2,\dots,n\}}\sum_{T\subseteq\{1,2,\dots,n\}}^{|S|=|T|}\mathrm{perm}(A_{S,T})\cdot\mathrm{perm}(B_{\bar S,\bar T})
\]

证明则考虑积和式的定义式:

\[\mathrm{perm}(A+B)=\sum_P\prod_{i=1}^n(A_{i,p_i}+B_{i,p_i})
\]

相当于对于每个 \(i\) 要么选 \(A_{i,p_i}\) 要么选 \(B_{i,p_i}\),枚举集合 \(S\),对于 \(i\in S\) 选 \(A_{i,p_i}\),\(i\in \bar S\) 选 \(B_{i,p_i}\)。则式子可以写为:

\[\mathrm{perm}(A+B)=\sum_{P}\sum_{S}\prod_{i\in S}A_{i,p_i}\prod_{j\in \bar S}B_{i,p_i}
\]

其中,\(S\) 对应的所有排列 \((p_i\mid i\in S)\) 就是把 \(S\) 中的元素拿来全部排列,\((p_i\mid i\in\bar S)\) 同理。于是可以得到上述结论,在该结论的基础上继续求解。

我们发现一个全 \(1\) 的方阵的积和式是易于计算的,即方阵边长的阶乘。于是考虑把原方阵 \(M\) 拆成全 \(1\) 方阵 \(A\) 和另一个方阵 \(B\) 的和,即

\[B_{i,j}=\begin{cases}
v_k-1&i=x_k,j=y_k\\
0&\text{otherwise}
\end{cases}
\]

则我们要计算的就是

\[\sum_{S}\sum_{T}^{|S|=|T|}\mathrm{perm}(B_{S,T})\cdot(|\bar S|!)
\]

考虑积和式的实际意义——每行每列恰选一个元素的乘积。这种「每行每列恰选一个」可以想到行列拆点建二分图,则对于所有 \(|S|=i\),\(\sum_{S}\sum_{T}^{|S|=|T|}\mathrm{perm}(B_{S,T})\) 就是在二分图上匹配 \(i\) 条边。

解法1

由于 \(B\) 上只有 \(k\) 个点非零,可以只考虑它们对应的行和列。

不难设计状压 DP,状压维护哪些行已经匹配过,枚举每一列再枚举一条边进行匹配,复杂度是 \(\mathcal O(2^kk)\),显然过不了。

注意到 \(k\) 并不大,如果能把 \(2^k\) 变成 \(2^{\frac k2}\) 就能够优化很多。实际上,由于边数只有 \(k\),所以连通块的大小至多 \(k+1\);这就意味着每个连通块中,点数较少的一侧的点只有 \(\frac k2\) 个。

于是对每个连通块单独 DP,状压点数较少的一侧。复杂度 \(\mathcal O(2^{\frac k2}k)\),更具体一些,设二分图点数为 \(n\),则复杂度为 \(\mathcal O(2^{\frac n2}k)\)。

仍然不能通过所有数据,但在连通块个数较多(意味着点数与边数的差较大)时表现优秀。

解法2

仍然考虑匹配。

如果每个连通块都是一棵树,则可以对每个连通块 \(T\) 做一次 \(\mathcal O(|T|^2)\) 的树形背包。

显然大多数情况下并不是树。我们尝试先生成森林,再决策哪些非树边要选,然后做树形背包。

非树边的决策没什么好方法,只能指数级枚举。但是注意到非树边的数量并不大,只有 \(k-(n-1)\)(\(n\) 是总点数)。复杂度 \(\mathcal O(2^{k-n}n^2)\)。

这个算法在 \(k-n\) 较小时表现更优——也就是连通块个数较少,点数与边数更接近。

正解

两个算法综合,平衡复杂度。主要是平衡指数级枚举的复杂度——

如果 \(n\le \frac 23k\),则采用算法1;否则采用算法2。最终复杂度 \(\mathcal O(2^{\frac k3}k^2)\)。


源代码

/* Lucky_Glass */
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm> #define CON(typ) const typ & const int M = 54, MOD = 1e9 + 7, N = 1e6 + 10; inline int add(int a, CON(int) b) { return (a += b) >= MOD ? a - MOD : a; }
inline int sub(int a, CON(int) b) { return (a -= b) < 0 ? a + MOD : a; }
inline int mul(CON(int) a, CON(int) b) { return int(1ll * a * b % MOD); }
int pPow(int a, int b) {
int r = 1;
while (b) {
if (b & 1) r = mul(r, a);
a = mul(a, a), b >>= 1;
}
return a;
} #define UPD(a, b, fun) a = fun(a, b) int rin(int &r) {
int c = getchar(); r = 0;
while (c < '0' || '9' < c) c = getchar();
while ('0' <= c && c <= '9') r = r * 10 + (c ^ '0'), c = getchar();
return r;
} struct Graph {
int head[M << 1], nxt[M << 1], to[M << 1], len[M << 1];
int cnt_edg;
inline void addEdge(CON(int) u, CON(int) v, CON(int) w) {
int p = ++cnt_edg, q = ++cnt_edg;
to[p] = v, nxt[p] = head[u], head[u] = p, len[p] = w;
to[q] = u, nxt[q] = head[v], head[v] = q, len[q] = w;
}
inline int operator [] (CON(int) u) const { return head[u]; }
}; Graph gr;
int n, m, cnt_x, cnt_y;
int id_x[N], id_y[N], edg[M][3], fac[N]; namespace DP_ON_BLOCK {
int cnt_e, cnt_f;
bool vis[M << 1];
int idx_e[M], idx_f[M], reid[M << 1], f[1 << 25], g[M]; void dfs(CON(int) u) {
vis[u] = true;
if (u <= cnt_x) idx_e[++cnt_e] = u;
else idx_f[++cnt_f] = u;
for (int it = gr[u]; it; it = gr.nxt[it])
if (!vis[gr.to[it]])
dfs(gr.to[it]);
}
/* cnt_a < cnt_b */
void doDP(CON(int) cnt_a, CON(int) cnt_b, int *idx_a, int *idx_b) {
for (int i = 1; i <= cnt_a; ++i) reid[idx_a[i]] = i - 1;
const int LIMITS = 1 << cnt_a;
for (int s = 0; s < LIMITS; ++s) f[s] = 0;
f[0] = 1;
for (int i = 1; i <= cnt_b; ++i) {
for (int s = LIMITS; ~s; --s)
for (int it = gr[idx_b[i]]; it; it = gr.nxt[it]) {
int v = reid[gr.to[it]];
if (!(s >> v & 1))
UPD(f[s | (1 << v)], mul(f[s], gr.len[it]), add);
}
}
int tmp_f[M] = {}, tmp_g[M] = {};
for (int s = 0; s < LIMITS; ++s) {
int pocnt = 0;
for (int i = 0; i < cnt_a; ++i) pocnt += s >> i & 1;
UPD(tmp_f[pocnt], f[s], add);
}
for (int i = 0; i <= cnt_a; ++i)
for (int j = 0; j <= m; ++j)
UPD(tmp_g[i + j], mul(tmp_f[i], g[j]), add);
for (int i = 0; i <= m; ++i) g[i] = tmp_g[i];
}
void main() {
for (int i = 1; i <= m; ++i)
gr.addEdge(edg[i][0], edg[i][1] + cnt_x, edg[i][2]);
g[0] = 1;
for (int i = 1; i <= cnt_x; ++i)
if (!vis[i]) {
cnt_e = cnt_f = 0;
dfs(i);
if (cnt_e < cnt_f) doDP(cnt_e, cnt_f, idx_e, idx_f);
else doDP(cnt_f, cnt_e, idx_f, idx_e);
}
int ans = 0;
for (int i = 0; i <= m; ++i) UPD(ans, mul(fac[n - i], g[i]), add);
printf("%d\n", ans);
}
} /* namespace DP_ON_BLOCK */ namespace DP_ON_TREE {
class Dsu {
private:
int fa[M << 1];
public:
void init(CON(int) siz) {
for (int i = 1; i <= siz; ++i)
fa[i] = i;
}
int find(CON(int) u) { return fa[u] == u ? u : fa[u] = find(fa[u]); }
inline bool merge(int u, int v) {
u = find(u), v = find(v);
if (u == v) return false;
fa[u] = v;
return true;
}
inline bool isRoot(CON(int) u) { return find(u) == u; }
}; int cnt_rem;
int rem_edg[M][3], f[M << 1][M][2], tmpf[M][2], g[M], tmpg[M];
bool isban[M << 1];
Dsu dsu; int dfs(CON(int) u, CON(int) fa) {
int sizu = 1;
for (int i = 0; i <= m; ++i)
f[u][i][0] = f[u][i][1] = 0;
f[u][0][0] = 1;
for (int it = gr[u]; it; it = gr.nxt[it])
if (gr.to[it] != fa) {
int v = gr.to[it];
int sizv = dfs(v, u);
for (int i = 0, lmt_i = sizu >> 1; i <= lmt_i; ++i)
for (int j = 0, lmt_j = sizv >> 1; j <= lmt_j; ++j) {
UPD(tmpf[i + j][0],
mul(f[u][i][0], add(f[v][j][0], f[v][j][1])), add);
if (!isban[u]) {
UPD(tmpf[i + j][1],
mul(f[u][i][1], add(f[v][j][0], f[v][j][1])), add);
if (!isban[v])
UPD(tmpf[i + j + 1][1],
mul(mul(f[u][i][0], f[v][j][0]), gr.len[it]), add);
}
}
sizu += sizv;
for (int i = 0, lmt_i = sizu >> 1; i <= lmt_i; ++i) {
f[u][i][0] = tmpf[i][0], f[u][i][1] = tmpf[i][1];
tmpf[i][0] = tmpf[i][1] = 0;
}
}
return sizu;
}
void main() {
dsu.init(cnt_x + cnt_y);
for (int i = 1; i <= m; ++i)
if (dsu.merge(edg[i][0], edg[i][1] + cnt_x)) {
gr.addEdge(edg[i][0], edg[i][1] + cnt_x, edg[i][2]);
} else {
rem_edg[cnt_rem][0] = edg[i][0], rem_edg[cnt_rem][1] = edg[i][1] + cnt_x;
rem_edg[cnt_rem][2] = edg[i][2];
++cnt_rem;
}
int ans = 0;
for (int s = 0; s < (1 << cnt_rem); ++s) {
for (int i = 1; i <= cnt_x + cnt_y; ++i) isban[i] = false;
bool iscrash = false;
int pocnt = 0, tot_mul = 1;
for (int i = 0; i < cnt_rem; ++i)
if (s >> i & 1) {
++pocnt, UPD(tot_mul, rem_edg[i][2], mul);
if (isban[rem_edg[i][0]]) { iscrash = true; break; }
if (isban[rem_edg[i][1]]) { iscrash = true; break; }
isban[rem_edg[i][0]] = isban[rem_edg[i][1]] = true;
}
if (iscrash) continue;
for (int i = 0; i <= m; ++i) g[i] = 0;
g[0] = 1;
int cnt_g = 0;
for (int u = 1, lmt_u = cnt_x + cnt_y; u <= lmt_u; ++u)
if (dsu.isRoot(u)) {
int sizu = dfs(u, 0) >> 1;
for (int i = 0; i <= sizu; ++i)
for (int j = 0; j <= cnt_g; ++j)
UPD(tmpg[i + j], mul(add(f[u][i][0], f[u][i][1]), g[j]), add);
cnt_g += sizu;
for (int i = 0; i <= cnt_g; ++i)
g[i] = tmpg[i], tmpg[i] = 0;
}
for (int i = 0; i <= cnt_g; ++i)
UPD(ans, mul(fac[n - pocnt - i], mul(g[i], tot_mul)), add);
}
printf("%d\n", ans);
}
} /* namespace DP_ON_TREE */ void init() {
fac[0] = 1;
for (int i = 1; i < N; ++i) fac[i] = mul(fac[i - 1], i);
}
int main() {
/*
freopen("input.in", "r", stdin);
freopen("debug.out", "w", stdout);
*/
init();
rin(n), rin(m);
for (int i = 1, x, y, w; i <= m; ++i) {
rin(x), rin(y), rin(w);
if (!id_x[x]) id_x[x] = ++cnt_x;
if (!id_y[y]) id_y[y] = ++cnt_y;
edg[i][0] = id_x[x], edg[i][1] = id_y[y], edg[i][2] = sub(w, 1);
}
if (3 * (cnt_x + cnt_y) <= 2 * m) {
/* std::cerr << "block" << std::endl; */
DP_ON_BLOCK::main();
} else {
/* std::cerr << "tree" << std::endl; */
DP_ON_TREE::main();
}
return 0;
}

THE END

Thanks for reading!

不愿成为你的过客记忆

我用字句

记录彼此点滴

如果最后失去

假装还是友情的默契

始终是咎由自取

——《语遥无期》By 夏语遥

> Link 语遥无期 - 网易云

「SOL」Permanent (Codeforces)的更多相关文章

  1. 一本通1648【例 1】「NOIP2011」计算系数

    1648: [例 1]「NOIP2011」计算系数 时间限制: 1000 ms         内存限制: 524288 KB [题目描述] 给定一个多项式 (ax+by)k ,请求出多项式展开后 x ...

  2. 「SCOI2016」背单词

    「SCOI2016」背单词 Lweb 面对如山的英语单词,陷入了深深的沉思,「我怎么样才能快点学完,然后去玩三国杀呢?」.这时候睿智的凤老师从远处飘来,他送给了 Lweb 一本计划册和一大缸泡椒,然后 ...

  3. loj2009. 「SCOI2015」小凸玩密室

    「SCOI2015」小凸玩密室 小凸和小方相约玩密室逃脱,这个密室是一棵有 $ n $ 个节点的完全二叉树,每个节点有一个灯泡.点亮所有灯泡即可逃出密室.每个灯泡有个权值 $ A_i $,每条边也有个 ...

  4. 「译」JUnit 5 系列:条件测试

    原文地址:http://blog.codefx.org/libraries/junit-5-conditions/ 原文日期:08, May, 2016 译文首发:Linesh 的博客:「译」JUni ...

  5. 「译」JUnit 5 系列:扩展模型(Extension Model)

    原文地址:http://blog.codefx.org/design/architecture/junit-5-extension-model/ 原文日期:11, Apr, 2016 译文首发:Lin ...

  6. JavaScript OOP 之「创建对象」

    工厂模式 工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程.工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题. function createPers ...

  7. 「C++」理解智能指针

    维基百科上面对于「智能指针」是这样描述的: 智能指针(英语:Smart pointer)是一种抽象的数据类型.在程序设计中,它通常是经由类型模板(class template)来实做,借由模板(tem ...

  8. 「JavaScript」四种跨域方式详解

    超详细并且带 Demo 的 JavaScript 跨域指南来了! 本文基于你了解 JavaScript 的同源策略,并且了解使用跨域跨域的理由. 1. JSONP 首先要介绍的跨域方法必然是 JSON ...

  9. 「2014-5-31」Z-Stack - Modification of Zigbee Device Object for better network access management

    写一份赏心悦目的工程文档,是很困难的事情.若想写得完善,不仅得用对工具(use the right tools),注重文笔,还得投入大把时间,真心是一件难度颇高的事情.但,若是真写好了,也是善莫大焉: ...

  10. 「2014-3-18」multi-pattern string match using aho-corasick

    我是擅(倾)长(向)把一篇文章写成杂文的.毕竟,写博客记录生活点滴,比不得发 paper,要求字斟句酌八股结构到位:风格偏杂文一点,也是没人拒稿的.这么说来,arxiv 就好比是 paper 世界的博 ...

随机推荐

  1. 【学习笔记】C++ 常量折叠原理和验证

    以下的代码很有意思,在相同时刻,相同的内存地址,数据居然会不一样. #include <iostream> int main(void) { const int const_val = 3 ...

  2. Vue.js 前端项目在常见 Web 服务器上的部署配置

    Web 服务器是一种用于存储,处理和传输Web内容的软件.它是一种特殊类型的服务器,具有处理 HTTP 请求并向浏览器返回 Web 页面和其他内容的能力.Web服务器支持多种编程语言,如 PHP,Ja ...

  3. (一)Abp入门

    ABP 是用于创建现代Web应用程序的完整体系结构和强大的基础架构,遵循最佳实践和约定,为 您提供 SOLID 开发经验. 目前 ABP 的版本   ASP.NET Boilerplate ASP.N ...

  4. git分支的一些处理情况记录

    一.开发分支(dev)上的代码更新后,要合并到 master 分支 git checkout dev #切换到dev分支 git pull #将远程更新的代码同步到本地 git checkout ma ...

  5. StartAllBack使用教程

    StartAllBack简介 StartAllBack是一款Win11开始菜单增强工具,为Windows11恢复经典样式的Windows7主题风格开始菜单和任务栏,功能包括:自定义开始菜单样式和操作, ...

  6. Windows Server 2016 安装AD和Exchange

    一.AD虚拟机操作 1.安装net framework 4.8 下载链接:https://dotnet.microsoft.com/download/dotnet-framework/net48 安装 ...

  7. 使用 DirectSound 播放 WAV 文件

    使用 DirectSound 播放 WAV 文件 本文需要的前置知识可以在之前的这两篇文章找到. WAVE音频文件格式及其64位扩展格式的简要介绍 读写wav格式文件 基于本文介绍的方法,我们也可以用 ...

  8. JZOJ 1967.【2011集训队出题】聪聪可可

    题目 [2011集训队出题]聪聪可可 思路 看看做做 阴阳 这道题 极力推荐 自从做了这道题后,这些题就变成秒切的题了 很容易想到求节点到分治中心的距离,然后 \(\bmod 3\) 那么在求根节点一 ...

  9. JZOJ 3432. 【GDOI2014模拟】服务器

    题目 解析 很容易想到的 \(dp\): 设 \(f_i\) 表示已经处理完 \(1..i\) 并且 \(i\) 是直接复制的需要的最小花费 那么 \(f_i=f_j+(i-j) \times (i- ...

  10. Hugging Face 每周速递: Space 支持创建模版应用、Hub 搜索功能增强、BioGPT-Large 还有更多

    每一周,我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新,包括我们的产品和平台更新.社区活动.学习资源和内容更新.开源库和模型更新等,我们将其称之为「Hugging Ne ...