【BZOJ1494】【NOI2007】生成树计数(动态规划,矩阵快速幂)
【BZOJ1494】【NOI2007】生成树计数(动态规划,矩阵快速幂)
题面
Description
最近,小栋在无向连通图的生成树个数计算方面有了惊人的进展,他发现:
·n个结点的环的生成树个数为n。
·n个结点的完全图的生成树个数为n^(n-2)。这两个发现让小栋欣喜若狂,由此更加坚定了他继续计算生成树个数的
想法,他要计算出各种各样图的生成树数目。一天,小栋和同学聚会,大家围坐在一张大圆桌周围。小栋看了看,
马上想到了生成树问题。如果把每个同学看成一个结点,邻座(结点间距离为1)的同学间连一条边,就变成了一
个环。可是,小栋对环的计数已经十分娴熟且不再感兴趣。于是,小栋又把图变了一下:不仅把邻座的同学之间连
一条边,还把相隔一个座位(结点间距离为2)的同学之间也连一条边,将结点间有边直接相连的这两种情况统称
为有边相连,如图1所示。
小栋以前没有计算过这类图的生成树个数,但是,他想起了老师讲过的计算任意图的生成树个数的一种通用方法:
构造一个n×n的矩阵A={aij},其中
其中di表示结点i的度数。与图1相应的A矩阵如下所示。为了计算图1所对应的生成数的个数,只要去掉矩阵A的最
后一行和最后一列,得到一个(n-1)×(n-1)的矩阵B,计算出矩阵B的行列式的值便可得到图1的生成树的个数所以
生成树的个数为|B|=3528。小栋发现利用通用方法,因计算过于复杂而很难算出来,而且用其他方法也难以找到更
简便的公式进行计算。于是,他将图做了简化,从一个地方将圆桌断开,这样所有的同学形成了一条链,连接距离
为1和距离为2的点。例如八个点的情形如下:
这样生成树的总数就减少了很多。小栋不停的思考,一直到聚会结束,终于找到了一种快捷的方法计算出这个图的
生成树个数。可是,如果把距离为3的点也连起来,小栋就不知道如何快捷计算了。现在,请你帮助小栋计算这类
图的生成树的数目。
Input
包含两个整数k,n,由一个空格分隔。k表示要将所有距离不超过k(含k)的结点连接起来,n表示有n个结点。
Output
输出一个整数,表示生成树的个数。由于答案可能比较大,所以你 只要输出答案除65521 的余数即可。
Sample Input
3 5
Sample Output
75
HINT
题解
这是一道神仙题啊。
鉴于我自己在网上研究了很久各种题解才知道怎么做。
我还是打算好好地把这道题目从头到尾写一写。
从部分分开始吧。
\(Task1\ 60pts:k\le 5,n\le 100\)
送分的一档
直接暴力构图,矩阵树定理直接算就好了
时间复杂度\(O(n^3)\)
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MOD 65521
#define MAX 111
inline int read()
{
RG int x=0,t=1;RG char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return x*t;
}
int n,K,c[MAX][MAX],ans=1;
int main()
{
K=read();n=read();
for(int i=1;i<=n;++i)
for(int j=i+1;j<=min(n,i+K);++j)
c[i][i]++,c[j][j]++,c[i][j]--,c[j][i]--;
for(int i=2;i<=n;++i)
for(int j=i+1;j<=n;++j)
while(c[j][i])
{
int t=c[i][i]/c[j][i];
for(int k=i;k<=n;++k)c[i][k]=(c[i][k]-1ll*c[j][k]*t%MOD)%MOD,swap(c[i][k],c[j][k]);
ans=-ans;
}
for(int i=2;i<=n;++i)ans=1ll*ans*c[i][i]%MOD;
printf("%d\n",ans);
return 0;
}
\(Task2\ 80pts:k\le 5,n\le 10000\)
这档分其实写出来基本就会满分了
发现\(k\)的范围十分的小,也就是每个点的边并不会很多
换而言之,如果一个点要和前面的所有点处在一个生成树中
意味着它必须和前面\(k\)个点中的至少一个处在同一个联通块中
所以,我们只需要考虑当前点和它前面\(K\)个点的状态就行了
那么,我们的状态是什么?
我们思考一下,对于生成树而言,我们比较在意的只有两个方面:联通块和边
对于相邻的\(K\)个点,单独拿出来显然是一个完全图,因此不需要考虑边的问题
所以,我们的状态和联通块有关。
现在的问题又变成了如何记录前面\(k\)个点的联通块呢?
我们可以按照顺序编号,保证任意一个编号的联通块在出现之前,所有小于它的编号都已经出现过。(最小表示法吼啊)
(不一定要这样,只需要一种能够保证所有相同的联通块方案只会被算一次就行了)
编完号之后,我们可以把前面\(k\)个点所在的联通块给压成十进制
因为\(k\le 5\),就只需要三个二进制位,所以可以用八进制来把联通块的情况给压成十进制位。
那么,不同的联通块方案数有多少呢?
相当于把\(n\)个球放进任意数量个集合,也就是\(Bell\)数,算出来是\(52\)种
我们不关心每个联通块之间的连接方案,我们只关心如何对于两种联通块之间进行转移
所以我们可以枚举当前点和前面\(k\)个点的连边方案(最多就\(k\)条边)
然后暴力(用并查集)判断是否成环,
同时,最前面的那个点也必须和当前这\(k\)个点中的一个在同一个联通块中
这样就可以转移到另外一个联通块的情况
再用上面的最小表示法把它的编号还原出来
这样证明可以从当前状态向后面的状态进行转移。
\(dp\)要的是初始状态和转移。
显然搞出来了转移,考虑初始状态。
显然不能一开始不满\(k\)个点
所以直接从\(k\)个点开始计算
因为每个联通块之间是完全图,所以可以暴力计算联通块大小为\(x\)时的方案数
那么\(k\)个点时,某个联通块情况的方案数就是\(\prod num_{size}\)
联通块大小为\(1,2\)时,有\(1\)种方法
联通块大小为\(3\)有\(3\)种
联通块大小为\(4\)有\(16\)种
联通块大小为\(5\)有\(125\)种。
这样子,我们就可以\(O(52*2^5*K^2+n*52^2)\)转移了
这样子可以过\(80pts\)
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MOD 65521
#define MAX 55
inline ll read()
{
RG ll x=0,t=1;RG char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=-1,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return x*t;
}
ll n;int K,cnt;
int p[1<<20],st[MAX];
bool check(int t)//检查一个状态是否合法
{
int tmp=1<<1;//因为第一个点一定属于一号联通快,所以先把一号联通快放进去检查
for(int i=3;i<K+K+K;i+=3)
{
for(int j=1;j<((t>>i)&7);++j)//检查比当前编号小的所有编号是否都已经出现过
if(!(tmp&(1<<j)))return false;
tmp|=1<<((t>>i)&7);//将当前编号也给放进来
}
return true;
}
void dfs(int x,int t)//暴力找出所有状态,每个编号利用3个二进制位存
{
if(x==K){if(check(t))p[t]=++cnt,st[cnt]=t;return;}
for(int i=1;i<=K;++i)dfs(x+1,t|(i<<(x+x+x)));
}
int fa[MAX],a[MAX];
int getf(int x){return x==fa[x]?x:fa[x]=getf(fa[x]);}
int f[11111][MAX],g[MAX][MAX];
int main()
{
K=read();n=read();dfs(1,1);
for(int i=1;i<=cnt;++i)
{
f[K][i]=1;memset(a,0,sizeof(a));
for(int j=0;j<K;++j)++a[(st[i]>>(j*3))&7];
for(int j=1;j<=K;++j)
if(a[j]==3)f[K][i]=3;
else if(a[j]==4)f[K][i]=16;
else if(a[j]==5)f[K][i]=125;
int t=st[i];
for(int s=0;s<(1<<K);++s)//暴力枚举当前点对于前面几个点的连边状态
{
for(int j=0;j<=K;++j)fa[j]=j;
for(int j=0;j<K;++j)//利用并查集维护联通性
for(int k=j+1;k<K;++k)
if(((t>>(3*j))&7)==((t>>(3*k))&7))
fa[getf(j)]=getf(k);
bool cir=false;
for(int j=0;j<K;++j)//检查当前点的连边
if(s&(1<<j))
{
if(getf(K)==getf(j)){cir=true;break;}//出现了环
fa[getf(K)]=getf(j);
}
if(cir)continue;//连边不合法
for(int j=1;j<=K;++j)//最前面的点必须和后面的一个点联通,否则就无法联通了
if(getf(0)==getf(j)){cir=true;break;}
if(!cir)continue;
int now=0,used=0;
for(int j=0;j<K;++j)//当前存在合法的联通方案,因此当前的状态可以转移到另外的一个状态上去
if(!(now&(7<<(j*3))))//当前点不在任意一个联通块中
{
now|=++used<<(j*3);//新的联通块
for(int k=j+1;k<K;++k)//把所有在一个联通块里的点丢到状态里去
if(getf(j+1)==getf(k+1))
now|=used<<(k*3);
}
g[i][p[now]]++;
}
}
for(int i=K+1;i<=n;++i)
for(int j=1;j<=cnt;++j)
for(int k=1;k<=cnt;++k)
if(g[j][k])f[i][k]=(f[i][k]+1ll*g[j][k]*f[i-1][j])%MOD;
printf("%d\n",f[n][1]);
return 0;
}
\(AC:n\le 10^{15}\)
明摆着\(log\)算法,
发现每次转移相同,直接矩阵快速幂就行了
时间复杂度\(O(logn·52^3)\),预处理的时间写在上面了。。
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MOD 65521
#define MAX 55
ll n;int K,cnt;
int p[1<<20],st[MAX];
struct Matrix
{
int s[MAX][MAX];
void clear(){memset(s,0,sizeof(s));}
void init(){clear();for(int i=1;i<=cnt;++i)s[i][i]=1;}
}G;
Matrix operator*(Matrix a,Matrix b)
{
Matrix ret;ret.clear();
for(int i=1;i<=cnt;++i)
for(int j=1;j<=cnt;++j)
for(int k=1;k<=cnt;++k)
ret.s[i][j]=(ret.s[i][j]+1ll*a.s[i][k]*b.s[k][j])%MOD;
return ret;
}
Matrix fpow(Matrix a,ll b)
{
Matrix s;s.init();
while(b){if(b&1)s=s*a;a=a*a;b>>=1;}
return s;
}
bool check(int t)//检查一个状态是否合法
{
int tmp=1<<1;//因为第一个点一定属于一号联通快,所以先把一号联通快放进去检查
for(int i=3;i<K+K+K;i+=3)
{
for(int j=1;j<((t>>i)&7);++j)//检查比当前编号小的所有编号是否都已经出现过
if(!(tmp&(1<<j)))return false;
tmp|=1<<((t>>i)&7);//将当前编号也给放进来
}
return true;
}
void dfs(int x,int t)//暴力找出所有状态,每个编号利用3个二进制位存
{
if(x==K){if(check(t))p[t]=++cnt,st[cnt]=t;return;}
for(int i=1;i<=K;++i)dfs(x+1,t|(i<<(x+x+x)));
}
int fa[MAX],a[MAX];
int getf(int x){return x==fa[x]?x:fa[x]=getf(fa[x]);}
int f[MAX],g[MAX][MAX];
int main()
{
scanf("%d%lld",&K,&n);dfs(1,1);
for(int i=1;i<=cnt;++i)
{
f[i]=1;memset(a,0,sizeof(a));
for(int j=0;j<K;++j)++a[(st[i]>>(j*3))&7];
for(int j=1;j<=K;++j)
if(a[j]==3)f[i]=3;
else if(a[j]==4)f[i]=16;
else if(a[j]==5)f[i]=125;
int t=st[i];
for(int s=0;s<(1<<K);++s)//暴力枚举当前点对于前面几个点的连边状态
{
for(int j=0;j<=K;++j)fa[j]=j;
for(int j=0;j<K;++j)//利用并查集维护联通性
for(int k=j+1;k<K;++k)
if(((t>>(3*j))&7)==((t>>(3*k))&7))
fa[getf(j)]=getf(k);
bool cir=false;
for(int j=0;j<K;++j)//检查当前点的连边
if(s&(1<<j))
{
if(getf(K)==getf(j)){cir=true;break;}//出现了环
fa[getf(K)]=getf(j);
}
if(cir)continue;//连边不合法
for(int j=1;j<=K;++j)//最前面的点必须和后面的一个点联通,否则就无法联通了
if(getf(0)==getf(j)){cir=true;break;}
if(!cir)continue;
int now=0,used=0;
for(int j=0;j<K;++j)//当前存在合法的联通方案,因此当前的状态可以转移到另外的一个状态上去
if(!(now&(7<<(j*3))))//当前点不在任意一个联通块中
{
now|=++used<<(j*3);//新的联通块
for(int k=j+1;k<K;++k)//把所有在一个联通块里的点丢到状态里去
if(getf(j+1)==getf(k+1))
now|=used<<(k*3);
}
g[i][p[now]]++;
}
}
for(int i=1;i<=cnt;++i)
for(int j=1;j<=cnt;++j)
G.s[i][j]=g[i][j];
G=fpow(G,n-K);
int ans=0;
for(int i=1;i<=cnt;++i)
ans=(ans+1ll*G.s[i][1]*f[i])%MOD;
printf("%d\n",ans);
return 0;
}
【BZOJ1494】【NOI2007】生成树计数(动态规划,矩阵快速幂)的更多相关文章
- bzoj1494 生成树计数 (dp+矩阵快速幂)
题面欺诈系列... 因为一个点最多只能连到前k个点,所以只有当前的连续k个点的连通情况是对接下来的求解有用的 那么就可以计算k个点的所有连通情况,dfs以下发现k=5的时候有52种. 我们把它们用类似 ...
- BZOJ1494 [NOI2007]生成树计数
题意 F.A.Qs Home Discuss ProblemSet Status Ranklist Contest 入门OJ ModifyUser autoint Logout 捐赠本站 Probl ...
- [BZOJ1494][NOI2007]生成树计数 状压dp 并查集
1494: [NOI2007]生成树计数 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 793 Solved: 451[Submit][Status][ ...
- poj 3744 Scout (Another) YYF I - 概率与期望 - 动态规划 - 矩阵快速幂
(Another) YYF is a couragous scout. Now he is on a dangerous mission which is to penetrate into th ...
- hdu 2604 Queuing(动态规划—>矩阵快速幂,更通用的模版)
题目 最早不会写,看了网上的分析,然后终于想明白了矩阵是怎么出来的了,哈哈哈哈. 因为边上的项目排列顺序不一样,所以写出来的矩阵形式也可能不一样,但是都是可以的 //愚钝的我不会写这题,然后百度了,照 ...
- Educational Codeforces Round 60 (Rated for Div. 2) - D. Magic Gems(动态规划+矩阵快速幂)
Problem Educational Codeforces Round 60 (Rated for Div. 2) - D. Magic Gems Time Limit: 3000 mSec P ...
- BZOJ4818 [SDOI2017] 序列计数 【矩阵快速幂】
题目分析: 一个很显然的同类项合并.注意到p的大小最大为100,考虑把模p意义下相同的求出来最后所有的减去没有质数的做矩阵快速幂即可. 代码: #include<bits/stdc++.h> ...
- 2019.02.11 bzoj4818: [Sdoi2017]序列计数(矩阵快速幂优化dp)
传送门 题意简述:问有多少长度为n的序列,序列中的数都是不超过m的正整数,而且这n个数的和是p的倍数,且其中至少有一个数是质数,答案对201704082017040820170408取模(n≤1e9, ...
- BZOJ2553 Beijing2011禁忌(AC自动机+动态规划+矩阵快速幂+概率期望)
考虑对一个串如何分割能取得最大值.那么这是一个经典的线段覆盖问题,显然每次取右端点尽量靠前的串.于是可以把串放在AC自动机上跑,找到一个合法串后就记录并跳到根. 然后考虑dp.设f[i][j]表示前i ...
- BZOJ5298 CQOI2018交错序列(动态规划+矩阵快速幂)
显然答案为Σkb·(n-k)a·C(n-k+1,k).并且可以发现ΣC(n-k,k)=fibn.但这实际上没有任何卵用. 纯组合看起来不太行得通,换个思路,考虑一个显然的dp,即设f[i][j][0/ ...
随机推荐
- Ubuntu系统python3版本设置问题
参照:https://blog.csdn.net/wangguchao/article/details/82151372
- Jmeter使用JDBC链接数据库进行压力测试
一.关于性能测试 对数据库进行压测时,我们需要关注的几个方面: 1.系统相关指标,诸如:系统CPU/内存/IO等 2.进程相关指标,诸如:mysql该数据库的对应的进程占用CPU/内存/IO等 3.数 ...
- Lua学习笔记(6): 函数
Lua的函数 函数用于简化程序,当某些工作需要重复执行的时候就可以使用函数减轻工作量(虽然复制粘贴也行) 语法: function 函数名(参数列表) 函数体 return 返回值 end --结束标 ...
- html的背景样式图片
背景图片 如果背景图片小于当前的div的情况下 默认的是将平铺充满元素 background-image 设置背景图片. background-repeat 设置是否及如何重复背景图片. repeat ...
- 《Learning scikit-learn Machine Learning in Python》chapter1
前言 由于实验原因,准备入坑 python 机器学习,而 python 机器学习常用的包就是 scikit-learn ,准备先了解一下这个工具.在这里搜了有 scikit-learn 关键字的书,找 ...
- Python基础知识-05-数据类型总结字典
python其他知识目录 1.一道题,选择商品的序号.程序员和用户各自面对的序号起始值 如有变量 googs = ['汽车','飞机','火箭'] 提示用户可供选择的商品: 0,汽车1,飞机2,火箭用 ...
- VisualSVN Server的配置和使用方法
VisualSVN Server的配置和使用方法 VisualSVN Server的配置和使用方法[服务器端] 安装好VisualSVN Server后[安装过程看这里],运行VisualSVN Se ...
- 智能客服 对话实现--python aiml包
利用了python的aiml包进行应答 什么是AIML? AIML是Richard Wallace开发的. 他开发了一个叫A.L.I.C.E(Artificial Linguistics Intern ...
- Java微笔记(7)
String 类常用方法 注意点: 字符串 str 中字符的索引从0开始,范围为 0 到 str.length()-1 使用 indexOf 进行字符或字符串查找时,如果匹配返回位置索引:如果没有匹配 ...
- Xcode 6添加模板无效
最近发现从Xcode 5拷贝来的模板在Xcode 6上是OK的,但是自己自定义的却不行,一直使用的是自定义的基类模板,最后发现原因是没有在 TemplateInfo.plist 中注册自定义的模板,注 ...