分类 课程 下的文章

DFTDiscrete Fourier Transformation离散傅里叶变换,适用于对采样性质的信号样本进行傅里叶变换。因为本人目前还没有系统地学习《信号与系统》这门课,对于傅里叶变换的原理不了解,因而会从感性的角度写下自己对于DFT的一些理解。

不论学不学《信号与系统》这门课,三角函数总是学过的。

接下来我们需要知道这么一个事实:有相当大一部分的信号,是可以通过一系列数学运算,使用一组周期不同的三角函数表示的。

信号需要满足狄氏条件。抛开这个复杂的条件,我们粗略地认为常见的声波(甚至电磁波,Asteroids@home曾今发布过DFT计算任务)都是符合条件的,因为至少目前我还没见过不符合条件的。这说明DFT具有一定的使用空间,进而具有研究意义,后续发明的、被誉为20世纪重要发明之一的FFT算法甚至被某些OI大佬拿去AK

由于DFT通常是给计算设备使用的,所以给定了输入长度有限的条件,下文假设有一个长度为N+1的信号序列x[n],均匀采样,总时长为T

设长度为N+1是为了能够方便地将每个采样点书写为x[0]x[1],...,x[N]。那么对于这个信号序列进行离散傅里叶变换,可以得到一个长度也为N+1的输出F[n]

这里使用傅里叶最原始的想法来表示信号在时域/频域的转换:

s_N[t] = \sum_{n=0}^{N} k\text{sin} (n\omega t + \theta)

而上述提到的,对信号x[n]进行离散傅里叶变换后得到的F[n],对应的是上式中的k\theta\omega则是通过采样频率\frac{N+1}{T}得到。

在进行实际计算后,可能会有一些发现:

  1. F[N]给出的是一个复数序列,即使输入是实数序列;
  2. x[n]是实数序列的时候,F[1]...F[N]是中心对称的,且对称的元素共轭(即a+bia-bi).

(1)是因为多出来的虚部和实部一起,共同记录了\theta中的信息,在实际还原成时域信号的过程中,虚部会在运算中消除的((2)从侧面印证了这一点)。

然后转到FFTW库的实现: http://www.fftw.org/doc/Multi_002dDimensional-DFTs-of-Real-Data.html#Multi_002dDimensional-DFTs-of-Real-Data

这里就提到,如果输入是实数的话,就会涉及到输出结果类型会变化的问题(从floatfftw_complex,实际上就是float[2])。

这里就正好与上述的两个发现对应,因为输出数据类型改变,因而需要重新规划空间;又因为数据存在冗余性,可以通过删除冗余数据的方式节省内存空间。

那么文档里的算术就可以理解了:F[n]一共需要保存的复数元素个数至少为\lceil \frac{(N+1)-1}{2} \rceil + 1个。当元素个数为奇数((n+1)%2==1)的时候,这个数是\frac{N}{2}+1,对应N+2个存储单元;偶数((n+1)%2==0)的情况下,这个数是\lceil \frac{N}{2} \rceil + 1,对应N+3个存储单元。由于原始的输入规模是N+1,因而在该规模为奇数或偶数的时候分别需要补充1或2个单位的数据规模,与文档中的叙述相符合。

这道题目有两种解法:记忆化搜索和线性动态规划。实际上,两者都属于动态规划的范畴之内,而记忆化搜索因为具有更为直接的思路,因而更容易想到(虽然不一定了解这个术语)。记忆化搜索的原理是利用搜索子问题的重复性,本质就是使用额外的空间存储已经进行过的搜索结果,是一种以空间换时间的思路,在此不做赘述。

本篇题记主要描述在本题中,将二维平面搜索转换为线性动态规划的一些理解。

第一步:分析问题结构

将问题动态规划化需要三个条件:最优子结构、无后效性和子问题重复。其中最优子结构的“问题最优解基于子问题的最优解”结论,从理论上允许我们通过求解子问题的最优解,从而推断出原问题的最优解;无后效性则确保了子问题一定是可解、易解的,从理论上杜绝了循环依赖的问题;子问题重复则是对于效率上的一种保证。前两个条件确保了动态规划的可行性,第三个则确保了动态规划的有效性。

但是在本题中,数据是按照二维坐标排列的。这样就会产生两个问题:

  1. 索引有两个维度,排序复杂;
  2. 现有的索引并不利于子问题的划分(即坐标与“高度”之间无关联性)。

于是注意到题目中,“斜坡”是按照点的高度进行定义的,所以考虑将点按照高度进行排序,这样恰好满足了最优子问题的结构:当考虑“斜坡”是以在现有“斜坡”的基础上追加新的、长度为1的小“斜坡”的话,恰好形成了一系列以高度为顺序的子问题序列。也就是说,这个时候,只需要按照高度顺序进行某种最长升序子序列的状态转移方程进行求解即可。

第二步:明确状态转移方程

状态转移方程可以视作一类递归函数,不过实现的时候用的是数组,而这也正是对子问题重复特性的利用。实际上,状态转移方程就是动态规划算法的实现原型。完成目标参数值下的状态转移方程值求解,也就完成了对问题的求解。

这里的状态转移思路非常简单,以当前坐标为起点的最长路径是:所有可到达该点、且高度高于该点的坐标中,所具有的最长路径。

公式表述如下:

L_{i} = \mathit{max}(L_{i}, L_{j} + 1)\ \text{where}\ \mathit{i, j}\ \text{is adjacent and}\ h_{i} > h_{j}

第三步:实现

基于上述思路,进行程序语言的书写就可以了。由于要设计高效算法,应该尽可能避免不必要的操作,在进行数据读入的时候就可以按照高度作为索引的数据结构进行保存。其余部分没有什么需要特别注意的,就是条件记得写对、不写漏就行。

在粗读 Reservoir-based sampling over large graph streams to estimate triangle counts and node degrees 论文的时候,第一句就读到不认识的东西,真是开幕雷击= =

在详细了解后,发现蓄水池采样算法本身是一个十分简单的算法,采用巧妙的方法解决了一个如下的“简单”问题:

如何在内容和长度均未知的数据流中,仅遍历一遍,且在O(n)的时间复杂度内,任意、随机地选出m个数据来?*

* 问题相较于参考资料有所改变,将在文尾具体解释。

长度和内容均未知的数据流带来了以下问题:

  1. 不一定能够使用存储设备完全储存(进一步说的话,可以特指内存);
  2. 不能预测且无法回看。

针对于题目的任意和随机性来看的话,上述第2点带来的问题就是,必须要对当前数据流中的任意元素进行等价的操作。也就是说,决定任何一个数据去或留(即是否加入最终选出的m个元素的集合)的时机,对于任何元素来说都应该是一样的。对于这种意义下的“操作时机”,我认为可以大致分为三类:

  1. “当机立断”:获取到该数据的同时就对于数据的去向进行决定。此时,程序的内存占用量是可预测的,(按照单线程考虑的话)同一时刻内不会存有两个数据流内的数据;
  2. “前看”:类似于编译原理中的前看原理,但是需要通过在程序中自行设立单独的“前看缓冲区”,通常来说,前看的深度是固定的;
  3. “我全都要”:最让人喜闻乐见的方式,全读入内存,慢慢折腾。

显然,3是首先要排除的。题目中有提到数据流的长度是未知的,很有可能超出当前可能的内存容量。

那么问题的解决策略只能在1或者2中着手。由于本人数学水平实在有限,无法断定并证明其中某种思路的可行性,因而在此,只能说明,蓄水池采样算法是使用思路1,借助随机函数实现对于任意长度序列的所及元素选择。

接下来对于算法,使用类Java伪代码进行描述:

InputStream in;                    // 数据流
Data current;                      // 当前数据
Data ans[] = new Data[m];          // 为要选出的元素预留空间
Random rand = new Random();        // 随机数产生器
Integer index = 0;                 // 已读取的数据数量计数

while(in.hasNext()) {
    current = in.next(); index++;  // 读取了一个数据
    if(index <= m) {
        // 先把“蓄水池”填满,此时不存在元素冲突问题
        // 下标=元素个数-1
        ans[index - 1] = current;
    } else {
        // 从当前已读取的数据编号范围内选出一个,把刚读入的数据“放到”这个位置
        // 这里的randInt取值区间是左闭右开的
        Integer slot = rand.nextInt(0, index);
        if(slot < m) {
            // 若是这个位置是前m个之内的话,替换数据
            ans[slot] = current;
        }
        /* 否则不做任何处理 */
    }
}
return ans;

然后就是简单扼要的原理说明了。

先说结论:使用蓄水池采样算法,可以在仅遍历一遍数据流、且在O(n)时间复杂度的情况下,使得每个数据被选出的概率均为\frac{m}{N}

证明如下:

将“数据能够在蓄水池中保留至结束”这一事件S(Select)分解为两个互相独立、串联的事件 [数据能够进入蓄水池I(In), 数据能够保留在蓄水池中K(Keep)],则可以表示如下:

P(S)=P(I) \times P(K)

从上面的算法描述代码可以看出,第1 \sim m个元素进入蓄水池的概率为1,即

\tag{1} \forall i \in \{1, 2, ..., m\}, P(I_{i}) = 1

而第1 \sim m个元素中的任意一个要保留到最终的话,假设数据流长度为N,则应该要在接下来N-m次数据读取的操作中均不被替换掉。由于第j个数据输入时共有j种选择,则每一次替换时,蓄水池中的第i个元素有\frac{j-1}{j}的概率不被替换。由此则有第1 \sim m中任意一个元素保留到最终的概率为

\tag{2} \forall i \in \{1, 2, ..., m\}, P(K_{i}) = \frac{m}{m+1} \times \frac{m+1}{m+2} \times ... \times \frac{N-1}{N} = \frac{m}{N}

(1)(2)可得

\tag{3} \forall i \in \{1, 2, ..., m\}, P(S_{i}) = P(I_{i}) \times P(K_{i}) = \frac{m}{N}

对于第m+1 \sim N个元素,由于此时蓄水池中已经装满元素了,需要以一定概率来替换已有元素,才能决定是否进入集合。由于新元素要进入集合只需替换原有集合中的任意一个即可,则有:

\tag{4} \forall i \in \{m+1, m+2, .... N\}, P(I_{i}) = \frac{m}{i}

可以发现此时位置不同的元素进入蓄水池的几率是随着位置向后推移而逐渐降低的。

继续计算这些元素保留到最终的概率,原理同公式(2)

\tag{5} \forall i \in \{m+1, m+2, ..., N\}, P(K_{i}) = \frac{i}{i+1} \times \frac{i+1}{i+2} \times ... \times \frac{N-1}{N} = \frac{i}{N}

此时由(4)(5)可得:

\tag{6} \forall i \in \{m+1, m+2, ..., N\}, P(S_{i}) = P(I_{i}) \times P(K_{i}) = \frac{m}{N}

合并(3)(6),神奇的事情发生了:

\forall i \in \{1, 2, ..., N\}, P(S_{i}) = \frac{m}{N}

证毕。

这也正是本算法的巧妙之处:不论原始数据流有多长,只要带选出的数据量m小于原始数据流的长度N,总能有办法随机、等概率地选出m个元素,即使N完全未知。

原文来自 https://www.jianshu.com/p/7a9ea6ece2af ,作者:邱simple。

在原文中,作者还讲述了分布式蓄水池抽样算法,并给出了Java代码的实现,如有需要可自行浏览。

这里简要说明一下开头提到的与原文中问题的区别。原文中问题还添加了“选出的元素不重复”这一限制条件,但是我认为,添加了该条件后,代码需要做出相应的改动(替换元素时的一些if条件),也不能称之为对原始数据流的等概率选取了,因而在本文中对命题进行了修改。如果有细心的读者发现了我的逻辑问题的话(万一有呢指读者),欢迎指正与交流。

看了题目之后,第一反应是有点像分形,对着样例画了一下,感觉和Sierpiński三角形很像。然后就觉得,虽然模拟是肯定能够完成任务的,但是能使用简单的数学规律表达的图形,肯定就存在简单的数学公式将其绘制出来,想了一阵子发现没想出来,无фак说,题解启动!

事实证明,方法是存在的,关键就在二进制。仔细观察题目中给出的样例数据,并不存在什么对称性质的规律,仔细核实后,发现图形和分形的Sierpiński三角形也是不完全一致的,但是可以发现的是,右半个正方形的01分布规律似乎和2的倍数存在某些不可描述的问题——实际上这就意味着,可能可以通过二进制解决这个问题。而这个时候,就要拓宽思维,把所有可能的表示都写出来,不要因为未知而束手束脚(指自己)。

话不多说,上图解决一切问题:

本图将这个二维点阵以0开始进行编址,给出其二进制数值,将标记为1的格子上色。仔细观察标黄的格子,不难发现,有一个很简单的特征,可以将上色和未上色的格子区分开来:所有上色的格子,对写成两行的二进制坐标进行或操作,得到“全1”,即111;未上色的格子,则得不到“全1”。为什么是三个1?这是由矩阵大小决定的,在这个样例中,矩阵大小为2^{3}

那么,最后就是设计一个点阵大小的循环,对于每一个坐标进行这个与计算,并且进行判断并输出0或1。因为指数函数增长实在太快,题中只给了一个很小的数据范围(10位),其中一个取巧的办法就是使用位运算,long long类型的甚至可以装到64位,虽然这里10位就够了。

绝活(套娃启动https://blog.csdn.net/qq_41593380/article/details/81146850

接下来试图用最简单的语言描述目前对并查集的理解。

前提:

  1. 并查集是一个算法,处理一组集合;
  2. 并查集可以查找两元素是否在同一集合;
  3. 并查集可以合并多个集合。(可以理解为,在要合并的多个集合中每次合并两个,最终只剩一个)

原理:

  1. 并查集中所有集合视作森林,单个集合视作树;
  2. 树最多是双层的;
  3. 通过树根节点的选举实现集合的合并。

实现:

  1. 可以使用数组简单地实现并查集:下标为元素编号(信息),存储的内容为当前元素(在树上)的父元素;
  2. 指向自己的元素为树根;
  3. 通过元素存储的父元素信息判断两个元素是否属于同一集合;
  4. 树的合并需要对某一集合的所有元素(包括树根)的父元素进行修改;
  5. 初始化并查集数据的时候就应该把所有的树结构缩减为双层的。