【ZJOI 2018】线图(树的枚举,hash,dp)
线图
题目描述
九条可怜是一个热爱出题的女孩子。
今天可怜想要出一道和图论相关的题。在一张无向图 $G$ 上,我们可以对它进行一些非常有趣的变换,比如说对偶,又或者说取补。这样的操作往往可以赋予一些传统的问题新的活力。例如求补图的连通性、补图的最短路等等,都是非常有趣的问题。
最近可怜知道了一种新的变换:求原图的线图 (line graph)。对于无向图 $G = ⟨V, E⟩$,它的 线图 $L(G)$ 也是一个无向图:
- 它的点集大小为 $|E|$,每个点唯一对应着原图的一条边。
- 两个点之间有边当且仅当这两个点对应的边在原图上有公共点(注意不会有自环)。 下图是一个简单的例子,左图是原图,右图是它对应的线图。其中点 $1$ 对应原图的边 $(1, 2)$,点 $2$ 对应 $(1, 4)$,点 $3$ 对应 $(1, 3)$,点 $4$ 对应 $(3, 4)$。
经过一些初步的摸索,可怜发现线图的性质要比补图复杂很多,其中突出的一点就是补图 的补图会变回原图,而 $L(L(G))$ 在绝大部分情况下不等于 $G$ ,甚至在大多数情况下它的点数和 边数会以很快的速度增长。
因此,可怜想要从最简单的入手,即计算 $L^{k}(G)$ 的点数(其中 $L^{k}(G)$ 表示对 $G$ 求 $k$ 次线图)。 然而遗憾的是,即使是这个问题,对可怜来说还是太困难了,因此她进行了一定的弱化。她给出了一棵 $n$ 个节点的树 $T$,现在她想让你计算一下 $L^{k}(T)$ 的点数。
输入输出格式
输入格式:
第一行输入两个整数 $n$,$k$,表示树的点数以及连续求线图的次数。
接下来 $n − 1$ 行每行两个整数 $u$, $v$ 表示树上的一条边。
输出格式:
输出一行一个整数,表示答案对 $998244353$ 取模后的值。
样例一
input
5 3
1 2
2 3
2 5
3 4
output
5
explanation
如下图所示,左图为原树,中图为 $L(G)$,右图为 $L^{2}(G)$。这儿并未画出 $L^{3}(G)$,但是由于 $L^{2}(G)$ 有 $5$ 条边,因此 $L^{3}(G)$ 中有 $5$ 个点。
限制与约定
时间限制:3s
空间限制:512MB
这题的数据范围很有趣啊,谁叫标算的复杂度也是指数级的呢?
假如用人类智慧是容易做出前 30 分的,高水平的选手再发现一些性质能拿到 50 分,而显然我并不是,因为我在考场上只有 20 分。
那我们来看一看线图的高妙之处:
考虑 $L^{k}(G)$ 中的每一个点在 $G$ 中代表的形状。
- 显然,$L(G)$ 的点数对应了原图中的一条边。
- 容易发现,$L^{2}(G)$ 的点数对应了原图中的一条折线(即一对相邻的两条边)。
- 稍加推敲得到,$L^{3}(G)$ 的点数对应了原图中的一条长度为三的链或一组相互相邻的三条边。
以此类推不难发现,在 $L^{k}(G)$ 中的每一个点对应的是 $G$ 中的一个不超过 $k$ 条边的联通导出子图。由于原图 $G$ 是棵树,所以 $L^{k}(G)$ 中每一个点对应的是 $G$ 中的一颗边数不超过 $k+1$ 的子树。一个简单的结论是:两个结构相同(即同构)的导出子图,它们在 $L^{k}(G)$ 中对应的节点个数一定也是相同的。
于是我们考虑了一个初步的做法:
- 枚举所有的边数不超过 $k+1$ 的无根树,假设为 $T_{i}$,算出 $T_{i}$ 在 $L^{k}(G)$ 中对应的点数 $w_{i}$。
- 算出 $T_{i}$ 在 $G$ 中出现了的次数 $t_{i}$。
- $Ans = \sum\limits_{T_{i}}^{} w_{i} * t_{i}$。
枚举不同构的有根树可以枚举括号序列然后用树哈希来去重,因此主要考虑给定 $T_{i}$,如何 求解 $w_{i}$ 和 $t_{i}$。
求解 $w_{i}$ :
因为 $T_{i}$ 的大小很小,我们能够求解 $L^{k}(T_{i})$ 的点数。但是这并不是我们要求的 $w_{i}$,因为这中的每一个点对应了 $T_{i}$ 中的每一个联通子图,也就是说这中间有 $T_{i}$ 的子图的贡献,我们需要减掉它们,我们可以 $O(2^{|T_{i}|})$ 枚举 $T_{i}$ 的子图,减掉它们的贡献,计算出 $w_{i}$ 的值。
容斥的复杂度并不是瓶颈,关键在于现在考虑如何求出 $L^{k}(T_{i})$ 的点数,如果暴力做的话,复杂度大概是 $O(k^{k})$,不太行。
我们可以沿用之前做部分分时的做法:
- $L(T_{i})$ 的点数是 $m$。
- $L^{2}(T_{i})$ 的点数是 $\sum\limits_{i \in V}^{} C_{d_{i}}^{2}$,其中 $d_{i}$ 为 $i$ 的度数。
- $L^{3}(T_{i})$ 的点数是 $\sum\limits_{(u,v) \in E}^{} (d_{u}-1)(d_{v}-1) + \sum\limits_{i \in V}^{} C_{d_{i}}^{3}$
- $L^{4}(T_{i})$ 的点数同样也可以用人类智慧直接算出来。
这样我们就只需要算 $L^{k-4}(T_{i})$ 就可以了。
这里有一个可以剪枝的地方,就是曾经算过的无根树与当前有同构时,就不必再算了。
求解 $t_{i}$:
想要直接把 $T_{i}$ 当无根树计算出它在另一个无根树 $G$ 中出现的次数是很难的,毕竟无根树的计数比有根树更复杂。
我们可以发现,如果我们把两颗无根树都当成有根树来做,即枚举有根树,却也是对的。因为无根树在另一颗无根树上一个成功的匹配,我们把某一棵无根树拉成有根树,另一棵无根树此时呈现出的有根形态一定会被枚举到恰好一次。于是问题得到了简化。
我们可以用树形dp直接解决它,由于 $T_{i}$ 的大小很小,用状压dp就可以了。
令 $f_{i,j}$ 为 $G$ 上第 $i$ 个点为第 $j$ 种有根树的根的嵌入方案数。
由于 $j$ 的孩子中可能存在两棵同构的子树,由于是没有标号的,故答案只能算一次,这是在dp时要注意的。然而我的实现方法并不优越,我的做法是,暴力合并所有同构的子树,每次dp时枚举之前所有状态中不包含这些同构子树中任意一个的状态。于是我就枚举子集了,复杂度变劣了一点。
然而在dp中有很多可以剪枝的地方,比如很显然,$i$ 的孩子数肯定不会少于 $j$ 的儿子数,或者在 $T_{i}$ 中存在一个子树和 $j$ 的子树同构,那可以不用重复计算了。
最后还有一个优化,就是当 $j$ 的亲生儿子中有叶子节点时,可以不用状压进去,直接用组合数算就可以啦。
于是我们就解决的这道题了,我在UOJ上勉强卡过去了,UOJ跑得还挺快的呢!反正我本地T飞了
#include <cstdio>
#include <map>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std; typedef long long LL;
typedef pair<int, int> Pair;
#define fir first
#define sec second
#define Mp make_pair
typedef vector<int>::iterator Vecit;
typedef vector<Pair>::iterator Vecpa; const int N = , MOD = , M = , NM = ; int Ans;
int n, K;
int fa[N], A[<<], pd[M][M], C[N][];
int dg[NM], dgr[NM], dgs[NM];
int dp[N][M], gp[][<<M]; vector<Pair> E, B[NM];
vector<int> G[N], T[N];
inline void Add(int a, int b) {
G[a].push_back(b);
} inline void Ad(int &a, int b) {
a+=b;
(a>=MOD)? (a-=MOD):();
}
inline int Last_pos(int x, int res=) {
for (x&=(-x); x; x>>=) ++res; return res;
}
inline int Count(int x, int res=) {
for (; x; x>>=) res+=x&; return res;
}
#define Sqr(x) ((LL)(x)*(x))
#define _c2(x) ((LL)(x)*((x)-1)/2) inline int Calc(int n, int m, int K, int res=) {
if (K==) return m;
memset(dg, , sizeof(int)*(n+));
memset(dgr, , sizeof(int)*(n+));
memset(dgs, , sizeof(int)*(n+));
for (Vecpa p=E.begin(); p!=E.end(); ++p) ++dg[(*p).fir], ++dg[(*p).sec];
if (K==) {
for (int *i=dg+; i<=dg+n; ++i) res=(res+_c2(*i))%MOD;
return res;
}
if (K==) {
for (Vecpa p=E.begin(); p!=E.end(); ++p)
res=(res+_c2(dg[(*p).fir]+dg[(*p).sec]-))%MOD;
return res;
}
if (K==) {
for (Vecpa p=E.begin(); p!=E.end(); ++p) {
int u=(*p).fir, v=(*p).sec, X=dg[u]+dg[v]-;
dgr[u]=(dgr[u]+(LL)X*X)%MOD;
dgr[v]=(dgr[v]+(LL)X*X)%MOD;
Ad(dgs[u], X);
Ad(dgs[v], X);
}
for (int i=; i<=n; ++i) {
int sqr=dgr[i], sum=dgs[i], deg=dg[i];
res=(res+(LL)sum*sum-sqr+(LL)sqr*(deg-)-(LL)*(deg-)*sum+(LL)*_c2(deg))%MOD;
}
return (LL)res*(MOD+)/%MOD;
}
int cnt=;
for (int i=; i<=n; ++i) B[i].clear();
for (Vecpa p=E.begin(); p!=E.end(); ++p) {
B[(*p).fir].push_back(Mp((*p).sec, ++cnt));
B[(*p).sec].push_back(Mp((*p).fir, cnt));
}
E.clear();
for (int i=; i<=n; ++i)
for (int j=; j<(int)B[i].size(); ++j)
for (int k=; k<j; ++k)
E.push_back(Mp(B[i][j].sec, B[i][k].sec));
return Calc(m, (int)E.size(), K-);
} inline void Get_dp(int x, int Fa, int K) {
for (Vecit p=G[x].begin(); p!=G[x].end(); ++p) if (*p!=Fa) {
Get_dp(*p, x, K);
}
for (int i=; i<=K; ++i) {
int flg=;
for (int j=; j<i; ++j) if (pd[i][j]) {
dp[x][i]=dp[x][j]; flg=; break;
}
if (flg) continue;
int cnt=;
vector<int> L;
for (Vecit p=T[i].begin(); p!=T[i].end(); ++p) if (*p!=fa[i]) {
if ((int)T[*p].size()==) ++cnt;
else L.push_back(*p);
}
if (cnt+(int)L.size() > (int)G[x].size()-(x!=)) continue;
int ST=<<(int)L.size(), ls=, no=;
memset(gp[], , sizeof(int)*(ST));
memset(gp[], , sizeof(int)*(ST));
gp[no][]=;
for (Vecit v=G[x].begin(); v!=G[x].end(); ++v) if (*v!=Fa) {
int ns=;
for (int j=; j<(int)L.size(); ++j, ns=) if (dp[*v][L[j]]){
for (int k=; k<j; ++k) if (pd[L[j]][L[k]]) ns|=<<k;
int U=(ST-)^ns^(<<j);
for (int sub=U; sub; sub=(sub-)&U) {
Ad(gp[ls][sub|ns|(<<j)], (LL)dp[*v][L[j]]*gp[no][sub|ns]%MOD);
}
Ad(gp[ls][ns|(<<j)], (LL)dp[*v][L[j]]*gp[no][ns]%MOD);
}
for (int st=; st<ST; ++st) {
Ad(gp[ls][st], gp[no][st]), gp[no][st]=;
}
no^=; ls^=;
}
dp[x][i]=(LL)gp[no][ST-]*C[(int)G[x].size()-(int)L.size()-(x!=)][cnt]%MOD;
}
} string Hash_tree(int x, int st, int Fa) {
vector<string> v;
for (Vecit p=T[x].begin(); p!=T[x].end(); ++p) {
if (*p!=Fa && (st>>(*p-))&) v.push_back(Hash_tree(*p, st, x));
}
sort(v.begin(), v.end());
string res="";
for (int i=; i<(int)v.size(); ++i) res+=''+v[i]+'';
return res;
} inline int Hash_to_int(string s) {
int res=, le=s.length();
for (int i=; i<le; ++i) res=(res<<)|(s[i]=='');
return res;
} inline int Build_tree(string tree, int K) {
int x=, cnt=, le=tree.length();
for (int i=; i<le; ++i)
if (tree[i]=='') {
x=fa[x];
if (!x) return -;
} else {
fa[++cnt]=x;
T[x].push_back(cnt);
T[cnt].push_back(x);
E.push_back(Mp(x, cnt));
x=cnt;
if (x>K) return -;
}
if (x!=) {
cerr << tree << endl;
exit();
}
return cnt;
} typedef unsigned long long ULL;
map<ULL, int> Map;
ULL get_hash(int n) {
ULL ret=;
vector<int> v;
for (int i=; i<=n; ++i) {
v.push_back(Hash_to_int(Hash_tree(i, (<<n)-, )));
}
sort(v.begin(),v.end());
for (int i=; i<n; ++i) ret=ret*+v[i];
return ret;
} void DFS(string tree, int K) {
if ((int)tree.length() < *(K-)) {
DFS(tree+'', K);
DFS(tree+'', K);
return;
}
E.clear();
for (int i=; i<=K; ++i) T[i].clear();
if (Build_tree(tree, K) != K) return;
int ss=Hash_to_int(Hash_tree(, (<<K)-, ));
if (~A[ss]) return;
ULL haha=get_hash(K);
A[ss]=(Map.count(haha)? Map[haha] : Map[haha]=Calc(K, K-, ::K)); for (int st=; st<(<<K)-; ++st) {
string gt=Hash_tree(Last_pos(st), st, );
if ((int)gt.length() != *(Count(st)-)) continue;
A[ss]=(A[ss]-A[Hash_to_int(gt)]+MOD)%MOD;
}
memset(pd, , sizeof pd);
for (int i=; i<=K; ++i)
for (int j=i+; j<=K; ++j)
pd[i][j]=pd[j][i]=(Hash_tree(i, (<<K)-, fa[i]) == Hash_tree(j, (<<K)-, fa[j]));
for (int i=; i<=n; ++i)
memset(dp[i], , sizeof(int)*(K+));;
Get_dp(, , K);
int res=;
for (int i=; i<=n; ++i) Ad(res, dp[i][]);
Ad(Ans, (LL)A[ss]*res%MOD);
} int main() {
memset(A, -, sizeof A);
scanf("%d%d", &n, &K);
for (int i=, x, y; i<n; ++i) {
scanf("%d%d", &x, &y); Add(x, y); Add(y, x);
} for (int i=; i<=n; ++i) {
C[i][]=;
for (int j=; j<=i && j<=; ++j) C[i][j]=(C[i-][j]+C[i-][j-])%MOD;
} for (int i=; i<=K+; ++i) DFS("", i);
printf("%d\n", Ans); return ;
}
【ZJOI 2018】线图(树的枚举,hash,dp)的更多相关文章
- [ZJOI 2018] 线图
别想多了我怎么可能会正解呢2333,我只会30分暴力(好像现场拿30分已经不算少了2333,虽然我局的30分不是特别难想). 首先求k次转化的点数显然可以变成求k-1次转化之后的边数,所以我们可以先让 ...
- luogu4383 [八省联考2018]林克卡特树(带权二分+dp)
link 题目大意:给定你 n 个点的一棵树 (边有边权,边权有正负) 你需要移除 k 条边,并连接 k 条权值为 0 的边,使得连接之后树的直径最大 题解: 根据 [POI2015]MOD 那道题, ...
- UOJ#373. 【ZJOI2018】线图 搜索,树哈希,动态规划
原文链接www.cnblogs.com/zhouzhendong/p/UOJ373.html 前言 真是一道毒瘤题.UOJ卡常毒瘤++.我卡了1.5h的常数才过QAQ Orzjry 标算居然是指数做法 ...
- 【BZOJ5211】[ZJOI2018]线图(树哈希,动态规划)
[BZOJ5211][ZJOI2018]线图(树哈希,动态规划) 题面 BZOJ 洛谷 题解 吉老师的题目是真的神仙啊. 去年去现场这题似乎骗了\(20\)分就滚粗了? 首先\(k=2\)直接算\(k ...
- matplotlib箱线图与柱状图比较
代码: # -*- coding: utf-8 -*- """ Created on Thu Jul 12 16:37:47 2018 @author: zhen &qu ...
- [八省联考2018]林克卡特树lct——WQS二分
[八省联考2018]林克卡特树lct 一看这种题就不是lct... 除了直径好拿分,别的都难做. 所以必须转化 突破口在于:连“0”边 对于k=0,我们求直径 k=1,对于(p,q)一定是从p出发,走 ...
- ZJOI 2018 一试记
ZJOI一试几天,天微冷,雨.倒是考试当天近午时分出了太阳. 开题前的一刻,心情反而平静了,窗外泛着淡金色的日光照进来,仿佛今天的我并不是所谓来冲击省队,而只是来经历一场洗礼. 开题了,虽然有一点小插 ...
- Tableau绘制K线图、布林线、圆环图、雷达图
Tableau绘制K线图.布林线.圆环图.雷达图 本文首发于博客冰山一树Sankey,去博客浏览效果更好.直接右上角搜索该标题即可 一. K线图 1.1 导入数据源 1.2 拖拽字段 将[日期]托到列 ...
- 一起来玩echarts系列(一)------箱线图的分析与绘制
一.箱线图 Box-plot 箱线图一般被用作显示数据分散情况.具体是计算一组数据的中位数.25%分位数.75%分位数.上边界.下边界,来将数据从大到小排列,直观展示数据整体的分布情况. 大部分正常数 ...
随机推荐
- 高可用OpenStack(Queen版)集群-8.Horizon集群
参考文档: Install-guide:https://docs.openstack.org/install-guide/ OpenStack High Availability Guide:http ...
- python2.7 倒计时
From: http://www.vitostack.com/2016/06/05/python-clock/#more Python公告 Python 发布了一个网站 http://pythoncl ...
- Java多线程编程之不可变对象模式
在多线程环境中,为了保证共享数据的一致性,往往需要对共享数据的使用进行加锁,但是加锁操作本身就会带来一定的开销,这里可以使用将共享数据使用不可变对象进行封装,从而避免加锁操作. 1. 模 ...
- 07慕课网《进击Node.js基础(一)》HTTP小爬虫
获取HTML页面 var http = require('http') var url='http://www.imooc.com/learn/348' http.get(url,function(r ...
- web153
电影网站:www.aikan66.com 项目网站:www.aikan66.com 游戏网站:www.aikan66.com 图片网站:www.aikan66.com 书籍网站:www.aikan66 ...
- Task 6.4 冲刺Two之站立会议9
今天主要对昨天用户提出的意见加以改进,虽然有些不能轻易实现但是仍然查阅了很多资料.因为他目前可以实现实时通信的功能,而我们想要在这个基础上实现临时的视频聊天的功能,但是时间有点紧迫,所以还没有实现.
- C#窗体随机四则运算 (第四次作业)
---恢复内容开始--- 增量内容:1)处理用户的错误输入,比如输入字母或符号等,处理除法运算中分母为0的情况,处理结果为负数的情况,保证是小学水平不出现负数,比如不能出现5-8=-3这种情况:2)用 ...
- Calculator项目的过程及感受
1.将Calculator项目传到Github上的链接地址:https://github.com/sonnypp/object-oriented/tree/master/Calculator 2.本次 ...
- 【搜索】POJ-2718 全排列+暴力
一.题目 Description Given a number of distinct decimal digits, you can form one integer by choosing a n ...
- Scrum 项目准备5.0
1.团队成员完成自己认领的任务. 2.燃尽图:理解.设计并画出本次Sprint的燃尽图的理想线.参考图6. 3.每日立会更新任务板上任务完成情况.燃尽图的实际线,分析项目进度是否在正轨. 每天的 ...