杂项
模拟赛和平时做题时不时有一些没见过的东西,写一下。
斯特林数
讲递推和初步性质。退役前可能学不到多项式。
第一类斯特林数:$s(n,m) $把 \(n\) 个元素分成 \(m\) 组圆排列的方案,圆排列是 \((n-1)!\)。
从 dp 意义上推递推公式,设 \(s(i,j)=dp_{i,j}\) 为 \(i\) 个元素分 \(j\) 组圆排列的方案。则第 \(i\) 个元素可以独立成组或者加入到先前组的某个元素旁边,有转移
初始化 \(s(0,0)=s(1,1)=1,s(i,1)=(i-1)!\)
这个东西还可以用来推上升幂下降幂相关,是多项式的内容,提一嘴。
其中 \(s_u()\) 就是 \(s()\),\(s_s(n,m)=(-1)^{n+m}s_u(n,m)\)
排个序从中抽若干个点 \(p_i,p_i>p_{i-1}\) 作为能看到的贡献点那剩下的那部分点可以围在这些 \(p_i\) 周围,\(n\) 会把答案分成 \(A-1+B-1\) 组,那就相当于用 \(n-1\) 个元素形成了 \(A+B-2\) 组环排列。答案即 \(\begin{bmatrix}n-1\\A+B-2\end{bmatrix}\binom{A+B-2}{A-1}\)
第二类斯特林数:\(S(n,m)\) 把 \(n\) 个不同的球放到 \(m\) 个相同盒子里的方案,求法是类似的,第 \(i\) 个球要么独立成盒要么放到先前某个盒子里。
然后捯饬一下可以解决一些小问题比如 \(n\) 个不同球放 \(m\) 个不同盒就考虑盒自排列即 \(m!S(n,m)\)。\(n\) 个球放到 \(m\) 个可以为空的盒子就是 \(\sum_{i=1}^mS(n,i)\)。
然后 \(n\) 个不同球放到 \(m\) 个可以为空的不同盒显然是 \(n^m\),用斯特林数表示可以是 \(\sum_{i=1}^mi!\binom{m}{i}S(n,i)\)。
然后这个就是斯特林展开即
模拟赛考的是树上全点对路径长 \(k\) 次方和,二项式定理展开维护一下 \(i\) 次方贡献跑点分治能过 \(10^5\),但是淀粉质感觉不太能做这题。这个题直接把换根 \(dp\) 写脸上了。
然后 \(i!*\binom{dis(x,y)}{i}\) 可以写成一个下降幂的形式并且下降幂有性质 \((x+1)^ {\underline i}=ix^{\underline {i-1}}+x^{\underline i}\)。那就可以维护 \(f_{u,i},dp_{u,i}\) 表示 \(u\) 子树到 \(u\) 和 \(u\) 它爹的 \(i\) 次下降幂和有转移
然后换个根就是 \(u->v\) 则 \(u\) 刨去 \(v\) 子树部分的距离要 \(+1\),剩下的不变,给每次 \(f_{v,i}\) 提供一个修正量
统计一下就好了。
斯坦纳树
以下内容出自我的题解。
这个题就是最小斯坦纳树的板题
考虑一个考场一眼想到的错解:跑最小生成树然后跑它的极小连通子树,错点在于最小生成树为了全点集的最优化而一定程度舍弃关键点集的最优化,进而枚举可行的点集一直跑最小生成树即可保证正确性。
现在考虑解决复杂度问题,能想到上文的错解建立在潜意识中的一个正确认知:答案的子图一定是一棵树。证明是容易的,边权全为正,对已有的一个联通关键点的树进行加边则一定不优。
再看树性质对求答案有何帮助,可以钦定一个关键点子集 \(S\) 形成的树(显然它可以包含非关键点)的根节点 \(u\),提出:讨论 \(u\) 的度数进行转移,在不清楚最终树的形态的前提下,不妨认为 \(u\) 的度数 \(deg\) 情况可以为 \(deg=1\) 或者 \(deg>1\)。
现在设 \(dp_{u,S}\) 表示根节点为 \(u\),关键点选择情况为 \(S\) 的斯坦纳树的答案。
对于前者,提供了一种转移方式:考虑给树添加一个 \(u\) 连接的节点 \(v\),无论如何 \(v\) 是一定加入到树中了,为了记录这个操作直接令 \(v\) 作为新的树根即 \(dp_{v,S}=min\{dp_{u,S}+w_{u,v}\}\)。可以用一个最短路算法实现本次 \(u\) 对整个图点集的一次答案更新。
另外,如果 \(v\) 已经在先前 \(u\) 的树中了那么这次转移一定不优。
对于后者,认为 \(u\) 一定可以作为一个中转点进行当前关键点集 \(S\) 的最优化,即对于 \(S\) 的每个子集 \(s\) 进行 \(dp_{u,S}=min\{dp_{u,s}+dp_{u,S-s}\}\) 的优化尝试。
#include<bits/stdc++.h>
#define int long long
#define MAXN 105
#define MAXM 505
#define N (1<<10)+5
using namespace std;
int n,m,k;
struct node{
int v,w,nxt;
}edge[MAXM<<1];
int h[MAXN],tmp;
inline void add(int u,int v,int w){
edge[++tmp]=(node){v,w,h[u]};
h[u]=tmp;
}
struct point{
int u,w;
bool operator <(const point &x)const{
return w>x.w;
}
};
priority_queue<point>Q;
int dp[MAXN][N];
bool vis[MAXN];
inline void dijkstra(int S){
memset(vis,0,sizeof(vis));
while(!Q.empty()){
point now=Q.top();
Q.pop();
int u=now.u;
for(int i=h[u];i;i=edge[i].nxt){
int v=edge[i].v,w=edge[i].w;
if(dp[u][S]+w<dp[v][S]){
dp[v][S]=dp[u][S]+w;
Q.push((point){v,dp[v][S]});
}
}
}
}
int inS[MAXN];
const int inf=1e18;
signed main(){
freopen("steiner.in","r",stdin);
freopen("steiner.out","w",stdout);
scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1,u,v,w;i<=m;i++){
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
int toT=(1<<k)-1;
for(int S=0;S<=toT;S++)for(int i=1;i<=n;i++)dp[i][S]=inf;
for(int i=1,u;i<=k;i++){
scanf("%lld",&inS[i]);
dp[inS[i]][1<<(i-1)]=0;
}
for(int S=1;S<=toT;S++){
for(int i=1;i<=n;i++){
for(int s=S;s;s=(s-1)&S)dp[i][S]=min(dp[i][S],dp[i][s]+dp[i][S^s]);
if(dp[i][S]==inf)continue;
Q.push((point){i,dp[i][S]});
}
dijkstra(S);
}
printf("%lld",dp[inS[1]][toT]);
return 0;
}
时间复杂度 \(O(n×3^k+mlogm×2^k)\),\(3^k\) 是因为 \(\sum_{i=0}^n\binom{i}{n}2^i=3^n\)
然对于斯坦纳树相关问题可以进行一定扩展比如这道题。
问题变为给定几个关键点群求出它们的斯坦纳森林。
不同颜色的关键点集间可以有共同边,跑若干次斯坦纳树的做法显然错误。
可以设 \(g_S\) 表示把关键点集 \(S\) 的点构成斯坦纳森林的答案,发现有以下性质:对任意 \(S\) 中包含的某个颜色 \(c_i\) 的关键点 \(p\),与它相同颜色的若干 \(p`\) 一定在 \(S\) 中,否则 \(S\)不合法。
进而考虑对一次斯坦纳树模版中 \(dp\) 过程中的关键点集 \(S\) 进行对 \(g\) 贡献的尝试,由上性质,发现这个 \(S\) 必须满足:完全包含了某个关键点颜色 \(c_i\) 的所有元素。可以则更新。
最后 \(g_S\) 也通过自己的子集合并更新,\(g_S=min\{g_s+g_{S-s}\},s\in S\)。
代码部分则是将模版中 dp 部分做上文修改。
for(int S=1;S<=toT;S++){
for(int i=1;i<=n;i++){
for(int s=S;s;s=(s-1)&S)dp[i][S]=min(dp[i][S],dp[i][s]+dp[i][S^s]);
if(dp[i][S]==inf)continue;
Q.push((point){i,dp[i][S]});
}
dijkstra(S);
bool f=1;
for(int i=1;i<=k;i++){
if(!ned[i])continue;
f&=!((S&ned[i])&&((S&ned[i])!=ned[i]));
}
if(!f)continue;
for(int i=1;i<=n;i++)ans[S]=min(ans[S],dp[i][S]);
}
for(int S=1;S<=toT;S++)
for(int s=S;s;s=(s-1)&S)
ans[S]=min(ans[S],ans[s]+ans[S^s]);
printf("%lld",ans[toT]);
在另一道题:
进行最短路松弛操作过程中可以顺带记录更新来源,最后输出方案。另外这道题的 dp 过程由于是点权所以稍有不同,详见luogu题解。
贪心: johnson 不等式 和 knuth 洗牌算法
在贪心单子里学到的神秘技巧,作记录。
给出三倍经验并说明题目三。先提一嘴 min-max容斥:\(max(a,b)=a+b-min(a,b)\)。介绍johnson不等式,或者叫邻项交换法,指的通过调整先前排序下的一组 \((a,b)\) 看是否更优。
比如题目三的一组先后加工的 \((x,y)\) 有
然后进行交换 \(x,y\) 的尝试,有
后者比前者更优满足
然后没了,上面的两道题就是把推导过程复杂化。
但是直接这么写comp函数是有问题的,原因是stl的排序函数需要满足该式子不满足的一些性质,不会证,见luogu第二篇题解。
总之就是这样的排序在先前序列顺序不同的情况下会出现不同的答案,所以有神人提出不断对序列打乱排序若干次后得到正确答案。然后就要引进knuth洗牌算法。
但是我太懒了所以给个链接。
平方的神奇转化
为一碟醋包盘饺子。给这样一道题,求所有给定长度的 \(O(n^2)\) 级别长度的序列,使得对于任意的 \(a_i\) 和 \(p_i=max_{j=1}^i a_j\) 满足 \(1\le a_i \le n,p_i\le p_{i-1}+1\)。然后求所有这样的序列中 \(1,2...n\) 出现次数的平方共 \(O(n)\) 个答案。
发现平方的存在根本无法 dp。然后就要把平方拆了。以下内容粘自我的题解。
考虑一个比较牛逼的转化:
则答案中 \(x\) 出现次数的平方形式被拆作:统计元素 \(x\) 出现次数(+k),统计出现的 \(k\) 次中能选出多少对 \(x\),然后乘二。
对于前者可以直接进行传统的计数 dp。对于后者,为了去重在统计第 \(i\) 位认定的 \(x\) 时认为对的第二个 \(x\) 出现在其后方,有 \((n-i)\) 种选法。
现在考虑如何实现计数dp。
挖掘性质:序列第 \(i\) 位的前缀极大值不超过上一位的前缀极大值+1。其中隐含一个条件:如果第 \(i\) 位的极大值是 \(x\) 则前 \(i-1\) 位必定出现过 \([1,x-1]\) 的所有数。
对第 \(i\) 位的元素 \(x\) 进行钦定,同时统计前 \(i\) 位极大值为 \(x\) 的方案数 \(f_{i,x}\),和知道前面极大值为 \(x\) 往后放 \(i\) 个数(放的数就可以大于x了) 的方案 \(g_{i,x}\)。那么"第 \(i\) 位是 \(x\)" 的方案数就是
就是要么 \(x\) 提供现在序列的极大值要么不提供。
这个计数过程就可以直接提供答案 \(+k\) 部分的贡献,继续设法统计组合上的贡献。
同前文所说的,令对的第二个元素在 \(x\) 后,有 \(n-i\) 种选法,值得注意的是此时后面的 \(n-i\) 个元素的某一个已经被这个 \(x\) 确定了,所以 \(g\) 的实际放个数 \(n-i-1\)
这一部分的贡献
加起来对每一位 \(i\) 统计一下 \(x\) 的答案就行了。
和哈希
我觉得不需要指名道姓哪道题了...前两天模拟赛的一题也可以用这个技巧。难点在于想到要用这个东西,问题是能拿来练的题根本找不到,所以很难想到要用。
实现没有难点,得分基本看脸。但是我脸黑,为了避免不必要的挂分,用这道不用指名道姓的题提供一个提高正确性的处理。
那么和hash就是把难以表示但容易拆解为公共特征的特殊状态用这个公共特征的hash值表示出来。比如星战里每个点一条出度的随机化权值和应当是一个固定且恰好可以表示答案状态的数,但如果就是冲突了就会很傻逼。进而考虑对某一个元素的权值赋值为其余所有元素随机化权值和的相反数,这样一个答案的权值一定是0,操作空间很大。
然后看模拟赛这道题。给定一个长度 \(O(nlogn)\) 到 \(O(n)\) 左右的数列 A,数列的元素取值范围为 \([1,m]\)。请计算有多少个非空子区间满足以下条件:该区间内每个元素的出现次数都相同(没有出现的元素视为出现 0 次)。
就可以把 \([1,m]\) 的每个数给一个随机化权值,显然合法区间应当是一个长为 \(km\) 且权值和为 \(k\) 倍 \([1,m]\) 权值和的段,枚举 \([1,m)\) 的起始点统计,开一个umap或者hashtable,复杂度就是 \(O(m\frac{n}{m}logV)=O(nlogV)\),哈希表加持下应当是 \(O(n)\)。
但你妈就是被卡了,卡正确性卡速度,\(O(n)\) 跑的比 \(O(nlogn)\) 慢几倍,同一份码交几遍分还能不一样的。我也觉得忍俊不禁。那就要想办法负优化,可以按上面的方式把一个权值设成其他权值和的相反数。那一段合法区间的权值一定是0了,把权值和排个序,相同段两两取个答案,\(O(nlogn)\) 跑的飞快,我去世。