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


题意

一个 \(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/C++ 设计模式 - 工厂模式(上)

    介绍说明 在年初七的时候,学习了工厂模式,今天在复习的时候发现漏了几个知识点,因此重写这篇文章,以循环渐进的描述方式来对比不同的使用技巧. 工厂设计模式属于 "创建型设计模式",在 ...

  2. 移动 WEB 布局方式之 rem 适配布局 ---- 苏宁首页案例制作

    一.技术选型 二.搭建相关文件夹结构 三.设置视口标签以及引入初始化样式 四.设置公共common.less 文件 common.less //设置常见的屏幕尺寸大小,修改里面的html 文字大小 / ...

  3. mysql 5.7安装教程及密码设置

    1.安装网址 https://dev.mysql.com/downloads/mysql/ 2.点击 Archives 3.切换版本,之前安装过新版本出现过很多问题,为了方便学习,所以选择了5.7这个 ...

  4. SpringBoot+Vue前后端分离项目,在过滤器取值为Null

    SpringBoot+Vue前后端分离项目,在过滤器取值为Null 是因为SessionID的问题,因为axios每次的请求都是一次新的sessionId,所以只需要在main.js下配置如下 axi ...

  5. Git03 自建代码托管平台-GitLab

    1 GitLab 简介 GitLab 是由 GitLabInc.开发,使用 MIT 许可证的基于网络的 Git 仓库管理工具,且具有wiki 和 issue 跟踪功能.使用 Git 作为代码管理工具, ...

  6. Consul调用no instances或Consul页面All service checks failing

    1.问题体现 Consul中Consumer调用Provider会出现No instances available for XXX 这时打开Consul控制台页面: 可以看到这里出现All servi ...

  7. 快学会这个技能-.NET API拦截技法

    大家好,我是沙漠尽头的狼. 本文先抛出以下问题,请在文中寻找答案,可在评论区回答: 什么是API拦截? 一个方法被很多地方调用,怎么在不修改这个方法源码情况下,记录这个方法调用的前后时间? 同2,不修 ...

  8. 我做的百度飞桨PaddleOCR .NET调用库

    我做的百度飞桨PaddleOCR .NET调用库 .NET Conf 2021中国我做了一次<.NET玩转计算机视觉OpenCV>的分享,其中提到了一个效果特别好的OCR识别引擎--百度飞 ...

  9. JZOJ 4754.矩阵

    \(\text{Problem}\) \(\text{Solution}\) 纪念我考场正解被二分暴力暴踩... 首先二分的话,显然可以二分出答案,然后数矩阵和大于等于本矩阵的是否有 \(k\) 个 ...

  10. 基于Python的OpenGL 06 之摄像机

    1. 引言 本文基于Python语言,描述OpenGL的摄像机 前置知识可参考: 基于Python的OpenGL 05 之坐标系统 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com) 笔 ...