普通视图

发现新文章,点击刷新页面。
昨天以前whtli的个人博客

多读单写场景下的thread-safe的map

作者 whtli
2023年8月6日 00:17

题目

基于HashMap,实现多读单写场景下的thread-safe的map

代码

import java.util.HashMap;import java.util.Map;import java.util.concurrent.Semaphore;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * 思路1:借助 ReadWriteLock 实现多读单写的线程安全map */class MultiReadSingleWriteMap1<K, V> {    private final Map<K, V> map = new HashMap<>();    // 使用ReadWriteLock实现读写锁    // 读操作使用读锁 readLock(),写操作使用写锁 writeLock()    private final ReadWriteLock lock = new ReentrantReadWriteLock();    /**     * get操作     * 类型:读     */    public V get(K key) {        // 读取时允许多个线程同时读取        lock.readLock().lock();        try {            return map.get(key);        } finally {            lock.readLock().unlock();        }    }    /**     * put操作     * 类型:写     */    public void put(K key, V value) {        // 写入时只有一个线程可以访问 Map        lock.writeLock().lock();        try {            // 新增/修改目标值            map.put(key, value);        } finally {            // 释放写锁            lock.writeLock().unlock();        }    }    /**     * remove操作     * 类型:写     */    public void remove(K key) {        // remove属于写操作,只有一个线程可以访问Map        lock.writeLock().lock();        try {            // 删除目标值            map.remove(key);        } finally {            // 释放写锁            lock.writeLock().unlock();        }    }}/** * 思路2:借助 synchronized、信号量 实现多读单写的线程安全map */class MultiReadSingleWriteMap2<K, V> {    // 定义读信号量、写信号量    private final Semaphore readLock;    private final Semaphore writeLock;    // 定义读计数器,用于判断占据和释放写锁的时机    private int readCount;    // 定义用于实际存储键值对的Map    private final Map<K, V> map = new HashMap<>();    /**     * 构造函数     */    public MultiReadSingleWriteMap2(int readLimit) {        this.readCount = 0;        // 读信号量初始化为输入上限值        this.readLock = new Semaphore(readLimit);        // 写信号量初始化上限为1        this.writeLock = new Semaphore(1);    }    /**     * get操作     * 类型:读     */    public V get(K key) throws InterruptedException {        // 获取读信号量        readLock.acquire();        try {            // 使用synchronized锁住临界区,对读计数器操作            synchronized (this) {                // 拿到读信号量,读线程计数器加1,占据写信号量                readCount ++;                if (readCount == 1) {                    // 因为写信号量只有1个,所以在读计数器等于1时占据一次就足够                    writeLock.acquire();                }            }        } finally {            readLock.release();        }        // 读取目标值        V value = map.get(key);        // 重新获取读信号量        readLock.acquire();        try {            synchronized (this) {                // 读取完毕,读线程计数器减1                readCount --;                // 如果读线程计数器为0,没有线程在读,可以释放写信号量                if (readCount == 0) {                    writeLock.release();                }            }        } finally {            readLock.release();        }        return value;    }    /**     * put操作     * 类型:写     */    public void put(K key, V value) throws InterruptedException {        // 尝试获取写信号量        writeLock.acquire();        try {            // 新增/修改目标值            map.put(key, value);        } finally {            // 释放写信号量            writeLock.release();        }    }    /**     * remove操作     * 类型:写     */    public void remove(K key) throws InterruptedException {        // 尝试获取写信号量        writeLock.acquire();        try {            // 删除目标值            map.remove(key);        } finally {            // 释放写信号量            writeLock.release();        }    }}

全排列问题(非递归实现和递归实现)

作者 whtli
2023年7月26日 23:39

题目

  • 给定一个不含重复数字的数组 nums ,返回其所有可能的全排列

代码

1. 非递归实现,借助栈

import java.util.ArrayList;import java.util.List;import java.util.Stack;/** * @author: Li Qiang * @date: 2023/7/26 * @description: 全排列问题的非递归实现 */public class Permutations {    public static void main(String[] args) {        Solution solution = new Solution();        int[] nums = {1, 2, 3, 4};        List<List<Integer>> permutations = solution.permute(nums);        // 输出验证        for (List<Integer> permutation : permutations) {            System.out.println(permutation);        }    }}class Solution {    public List<List<Integer>> permute(int[] nums) {        int n = nums.length;        // 定义结果列表        List<List<Integer>> ans = new ArrayList<>();        // 借助栈模拟递归过程        Stack<List<Integer>> stack = new Stack<>();        stack.push(new ArrayList<>());        while (!stack.isEmpty()) {            // currentPath代表当前的排列(不一定是全排列)            // 其中的元素代表已将被当前排列采用的元素            List<Integer> currentPath = stack.pop();            if (currentPath.size() == n) {                // 如果当前的排列是全排列,将其添加到结果列表中                ans.add(currentPath);            } else {                // 如果当前的排列没有完成,通过添加未使用的元素来补充它                for (int element : nums) {                    if (!currentPath.contains(element)) {                        // 如果当前排列的元素中不包含element,则采纳当前数组元素并将其放进新排列中                        List<Integer> newPath = new ArrayList<>(currentPath);                        newPath.add(element);                        // 新的排列(不一定是全排列)暂时压入栈中                        stack.push(newPath);                    }                }            }        }        return ans;    }}

2. 递归实现

/** * @author: Li Qiang * @date: 2023/7/26 * @description: 全排列问题的递归实现 */class Solution {    List<List<Integer>> ans;    boolean[] visited;    List<Integer> path;    public List<List<Integer>> permute(int[] nums) {        int n = nums.length;        ans = new ArrayList<>();        visited = new boolean[n];        path = new ArrayList<>();                loop(nums, 0, n);        return ans;    }    public void loop(int[] nums, int len, int n) {        if (len == n) {            ans.add(new ArrayList<>(path));            return;        }        for (int i = 0; i < n; i ++) {            if (visited[i] == true) {                continue;            } else {                visited[i] = true;                path.add(nums[i]);                loop(nums, len + 1, n);                // 回溯                path.remove(path.size() - 1);                visited[i] = false;            }        }    }}

SpringBoot项目使用OSHI获取系统状态信息报错

作者 whtli
2023年4月13日 22:00

SpringBoot项目,使用OSHI获取系统状态信息时,后端打印正常,返回至前端的过程中遇到了以下两个报错

  1. java.lang.IllegalStateException: Unmapped relationship: 7
  2. COM exception querying MSAcpi_ThermalZoneTemperature, which might not be on

解决方式如下:

1. java.lang.IllegalStateException: Unmapped relationship: 7

解决方式:添加依赖net.java.dev.jna

<!-- OSHI --><dependency>    <groupId>com.github.oshi</groupId>    <artifactId>oshi-core</artifactId>    <version>5.2.5</version></dependency><!-- jna --><dependency>    <groupId>net.java.dev.jna</groupId>    <artifactId>jna-platform</artifactId>    <version>5.10.0</version></dependency>

2. COM exception querying MSAcpi_ThermalZoneTemperature, which might not be on

解决方式:将获取到的系统状态信息转为String或者整型后再添加到VO中返回

(1)业务逻辑代码
import com.geo.integrated.model.vo.SystemStatus;import com.geo.integrated.service.VisualStatusService;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Service;import oshi.SystemInfo;import oshi.hardware.*;import oshi.software.os.OperatingSystem;@Service@Slf4jpublic class VisualStatusServiceImpl implements VisualStatusService {    /**     * 获取系统状态信息     *     * @return 系统状态信息     */    @Override    public SystemStatus getSystemState() {        // 系统信息        SystemInfo systemInfo = new SystemInfo();        // 操作系统信息        OperatingSystem operationSystemInfo = systemInfo.getOperatingSystem();        // 硬件信息        HardwareAbstractionLayer hardwareInfo = systemInfo.getHardware();        /*有了代表硬件信息的对象HardwareAbstractionLayer之后,就可以获取硬件相关的信息了*/        // 内存相关信息        GlobalMemory memoryInfo = hardwareInfo.getMemory();        // 内存总容量        String totalMemory = String.valueOf(memoryInfo.getTotal());        // 可用内存的容量        String availableMemory = String.valueOf(memoryInfo.getAvailable());        /*有了内存总容量和内存可用容量,就可以计算出当前内存的利用率了*/        // CPU相关信息        CentralProcessor processor = hardwareInfo.getProcessor();        // CPU型号        String processorName = processor.getProcessorIdentifier().getName();        // 物理CPU数        int physicalPackageCount = processor.getPhysicalPackageCount();        //物理核心数        int physicalProcessorCount = processor.getPhysicalProcessorCount();        SystemStatus systemStatus = new SystemStatus(String.valueOf(systemInfo),                String.valueOf(operationSystemInfo),                String.valueOf(hardwareInfo),                String.valueOf(memoryInfo),                Long.parseLong(totalMemory) / 1000 / 1000 / 1000,                Long.parseLong(availableMemory) / 1000 / 1000 / 1000,                String.valueOf(processor), String.valueOf(processorName),                Integer.parseInt(String.valueOf(physicalPackageCount)),                Integer.parseInt(String.valueOf(physicalProcessorCount)));        return systemStatus;    }}
(2)VO代码
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructor@AllArgsConstructorpublic class SystemStatus {    /**     * 系统信息     */    private String systemInfo;    /**     * 操作系统信息     */    private String operationSystemInfo;    /**     * 硬件信息     */    private String hardwareInfo;    /**     * 内存相关信息     */    private String memoryInfo;    /**     * 内存总容量     */    private Long totalMemory;    /**     * 可用内存的容量     */    private Long availableMemory;    /**     * CPU相关信息     */    private String processor;    /**     * CPU型号     */    private String processorName;    /**     * 物理CPU数     */    private Integer physicalPackageCount;    /**     * 物理核心数     */    private Integer physicalProcessorCount;}

Estimating High-Dimensional Directed Acyclic Graphs with the PC-Algorithm

作者 whtli
2022年10月28日 21:00

原文:Kalisch M, Buehlmann P. Estimating high-dimensional directed acyclic graphs with the PC-algorithm. J Mach Learn Res 2007;8:613–36.
原文网页版
Web of science

Abstract

本文研究了PC算法用于估计具有相应高斯分布的高维有向无环图(DAG)的骨架和等价类。 对于有许多节点(变量)的稀疏问题,PC算法在计算上是可行的,而且往往非常快,它具有吸引人的特性,可以自动实现高计算效率,作为真实底层DAG的稀疏程度的函数。本文证明了该算法在高维稀疏DAGs中的一致性,并允许节点数量随样本大小n快速增长,对于任何0 < a <∞快如$O(n^a)$。稀疏性假设是相当最小的,只要求DAG中的邻域的阶数低于样本容量n。文章还演示了模拟数据的PC算法。

1. Introduction

图模型是一种用来分析和可视化随机变量之间的条件独立性关系流行的概率工具。模型的主要构建模块是节点(代表随机变量)和边(编码了顶点的条件独立性关系)。随机变量之间的条件独立性结构可以使用马尔可夫性质来探索。

当前的研究兴趣是有向无环图(DAG),它包含有向边而不是无向边,这在一定程度上限制了条件独立性关系。这些图可以应用马尔可夫性质来解释。当忽略DAG的方向,可以得到一个DAG的骨架。通常来说,它和条件独立性图(CIG)是不同的,见2.1节(因此,有向图的估计方法不能简单地借鉴无向的CIG的估计方法)。2.1节中可以看到,骨架可以很容易地解释,从而对数据的依赖结构产生有趣的见解。

由于DAG空间的巨大规模,从数据中估计DAG是困难的,在计算上也是不可行的:可能的DAG的数量在节点数量上是超指数的。然而,针对中小规模节点数量,有一些十分成功的的 搜索-评分方法。例如,搜索空间可能像MWST那样被限制为树结构,或者采用贪婪的搜索方式。如GES (Greedy Equivalent Search, see Chickering, 2002a) 方法所述,贪婪的DAG搜索可以通过利用概率等价关系来优化,且搜索空间可以从单个DAG缩小到等价类。尽管这种方法在中小规模的节点数量情况下似乎很有前途,但它受限于一个事实,即等价类的空间在节点数量增长时也是超指数增长的 (Gillispie and Perlman, 2001)。

一个有趣的替代贪婪或者结构限制的方法是Spirtes等人在2000年提出的PC算法。它从一个完备的无向图开始,基于条件独立性决策递归地删除边。这会生成一个无向图,然后它会被部分地定向,并进一步扩展以表示底层的DAG。PC算法在最坏的情况下是以运行时间是运行的,但是如果真实的底层DAG是稀疏的(这通常是一个合理的假设),运行时间将会缩减为多项式时间。

在过去,提出了一些有趣的混合方法,最近,Tsamardinos等人(2006)提出了一种计算上非常有竞争力的算法。本文还参考了他们的论文,在广泛的算法之间进行了相当详尽的数值比较研究。

本文主要研究了在高维环境下DAGs的等价类和骨架的估计(对应于多元高斯分布),即节点数p可能远远大于样本数n。本文证明,当样本大小n→∞时,即使允许维数 $p = p_n = O(n^a) (0 ≤ a <∞)$作为n的函数快速增长,PC算法也能一致地估计出底层稀疏DAG地等价类和骨架。

如第4.5节所示,本文对PC算法的实现速度惊人地快,它允许估计一个稀疏的DAG,即使p很大。对于p远大于n的高维设定,底层DAG的稀疏性对于统计一致性和计算可行性是至关重要的。本文的分析似乎是第一次为高维DAG建立了一个可证明的正确算法(在渐进意义上),该算法在计算上是可行的。

关于包括PC算法在内的一类方法的一致性问题已经被Sprites等人和Robins等人在因果推断的文章讨论过。他们证明,只假设忠实性(第二节中有说明),统一一致性无法实现,但点状一致性可以实现。在本文中,本文用两种方式对其进行了扩展:本文提供了一套假设,使PC算法具有统一的一致性。更重要的是,本文证明即使是当节点数和邻居数增加,并且最小的非零协方差作为样本量的函数而减小,这个一致性也能始终保持。Zhang和Spirtes(2003)也提出了比忠实性条件更严格的假设,使均匀一致成为可能。Zuk等人(2006)对学习正确的贝叶斯网络结构需要多少样本的更普遍的讨论。

寻找DAG的等价类的问题与特征选择问题有很大的重合:如果找到了等价类,则可以很容易地读取任意变量(节点)的马尔可夫毯。给定一个节点集合V,假设M是节点X的马尔科夫毯,那么在给定M的条件下,XV\M是条件独立的。因此,M包含且只包含所有的X的相关特征。例如,见Goldenberg和Moore(2004)关于处理非常高的维度的方法,或Ng(1998)关于处理泛化误差的界限的相当普遍的方法。

2. Finding the Equivalence Class of a DAG

在本节中,首先说明主要的概念。然后,给出关于PC算法的详尽描述。

2.1 Definitions and Preliminaries

G=(V, E) 由一组顶点V={1,…,p} 和一组边 E⊆V×V 组成,即边集是不同节点的有序对的子集。在本文的设定中,节点集对应于随机变量X∈R^p^ 的分量。如果 (i, j)∈E(j, i)/∈E ,则边 (i, j)∈E 被叫做有向边,用符号i→j 表示。如果 (i, j)∈E(j, i)∈E ,则该边被叫做无向边用符号 i-j 表示。一个有向无环图(DAG)是一个所有边都是有向边且不包含环的图。

如果存在有向边 i→j ,则节点i是节点 j 的父节点。节点j的父节点的集合用 pa(j) 表示。节点 j 在图 G 中的邻居集合用 adj(G, j) 表示,它表示所有直接和 j 通过边(有向或者无向)连接的节点。 adj(G, j) 中的节点也被称为j的邻居或者与 j 相邻。

如果 $R^p$上 的概率分布 P 中的条件独立性可以从图 G 的d-separation中被推断出来,反之亦然,则 P 忠诚于图 G 。更精确的说:考虑一个随机向量 X~PP 的忠诚性意味着:对于任意的 i,j∈Vi≠j ,则对于任意的 s⊆V

$X^{(i)}$ and $X^{(j)}$ are conditionally independent given ${X^{(r)}; r ∈ s}$

⇔ node i and node j are d-separated by the set s.

d-separation的概念可以由道德图定义;详见Lauritzen的描述 (1996,Prop. 3.25)。在此指出,忠实性是排除某些类别的概率分布的。Spirtes等人(2000,第3.5.2章)给出了一个非忠实分布的例子。另一方面,多元正态族(本文将限制在此)的非忠诚分布在与DAG G相关的分布空间中形成一个Lebesgue 空集,见Meek(1995a)。

DAG G 的骨架是用无向边代替 G 中的有向边得到的无向图。DAG G 中的 v-structure 是一个有序的三元节点组 (i, j, k) 使得G包含有向边 i→jk→j ,并且 ik 在G中不相邻。

众所周知,对于一个由DAG G 生成的概率分布 P ,存在一个完整的具有对应分布 P 的等价类DAGs (见 Chickering, 2002a, Section 2.2 )。即使有无限多的观察结果,本文也无法区分一个等价类中的不同DAG。利用Verma和Pearl(1990)的一个成果,可以更精确的描述等价类的特征。当且仅当两个DAG有相同的骨架和相同的v-structure时,他们是等价的。

常用的DAG等价类的可视化工具是完备的部分有向无环图(CPDAG)。一个部分有向无环图(PDAG)是一个部分边有向且部分边无向的图。PDAG之间或PDAG和DAG之间的等价性可以通过检查骨架和V形结构来决定,就像DAG一样。一个PDAG是完备的,如果:(1)在属于DAG等价类的每个DAG中也存在相应的有向边,且(2)对于每条无向边 i−j ,在等价类中存在一个带i→j的DAG和一个带 i←j 的DAG。

CPDAG编码了相应的等价类中包含的所有独立性信息。Chickering(2002)证明,当且仅当两个CPDAG表示的是同一个等价类时,它们是等价的,即,它们表示的是同一个等价类。

尽管主要目标是确定CPDAG,骨架本身已经包含了有趣的信息。尤其是,如果概率分布P忠诚于一个DAG G,

there is an edge between nodes i and j in the skeleton of DAG G

⇔ for all $s ⊆ V \ {i, j}, X^{(i)}$ and $X^{(j)}$ are conditionally dependent given ${X^{(r)}; r∈s}$,(1)

(Spirtes et al., 2000, Th. 3.4). 这表明如果概率分布P对于一个DAG G来说是忠诚的,则DAG G的骨架是对应于P的条件独立性图(CIG)的真子集(或子集)。原因是CIG中的一条边只需要在给定集合V \ {i, j} 的情况下有条件依赖性。更重要的是,骨架张的每条边都表示某种强依赖,其不能通过其他变量来解释。本文认为,这个性质对探索性分析很有意义。

在接下来的细节内容中将看到,估计CPDAG主要由两部分组成(这自然地组成了本文地分析结构):(1)骨架的估计(2)部分边的定向。所有统计推断都在第一部分完成,第二部分只是对第一部分的结果应用确定性的规则。因此本文将更多的注意力放在第一部分上,如果第一部分操作正确,那么第二部分将永远不会失败。但是,如果在第一部分中出现错误,第二部分将对它更加敏感,因为它更详细地依赖于(1)的推断结果。当处理高维设定时(p大,n小),CPDAG比骨架的恢复更加困难。此外,对于CPDAG的解释更多地依赖于图的全局正确性。而对于骨架的解释只依赖于局部区域,因此更可靠。

本文的结论是,如果真实的底层概率机制是由DAG生成的,那么找到CPDAG是主要目标。骨架本身通常已经提供了有趣的见解,在高维设定中,当找到一个有用的CPDAG的近似似乎没有希望时,使用无向骨架作为CPDAG的替代目标可能是有趣的。

综上所述,本文接下来将描述两个主要步骤。首先,文章讨论PC算法生成骨架图的部分。之后文章将通过讨论寻找CPDAG的扩展来完成该算法。在讨论PC算法理论性质的时,文章将使用相同的格式。

2.2 The PC-algorithm for Finding the Skeleton

寻找骨架的一种朴素的策略是检查给定所有子集s⊆V \ {i, j}(见公式1)的”条件独立性”,也就是说,如Verma和j . pearl(1991)首次提出的,在多元正态分布情况下的所有”偏相关”。当p大于样本量时,这在计算上是不可行的,在统计上也是不恰当的。PC算法使用了一个更好的办法,它可以利用图的稀疏性。更准确地说,文章应用PC算法的一部分来识别DAG的无向边。

2.2.1 POPULATION VERSION FOR THE SKELETON

在PC算法地population版本中,文章假设对所有必要地条件独立性都有充分地了解。这里指的PC算法是别人所说的PC算法的第一部分;另一部分在第2.3节的算法2中描述。

image-20221028194614633

PC算法的第一部分在Algorithm1中给出。算法1中 l 的最大值可表示为

$m_{reach}$ = maximal reached value of l. (2)

$m_{reach}$的值取决于底层分布。

从Spirtes等人(2000)中的定理5.1中,可以很容易地推导出该算法会生成正确的骨架。文章总结如下。

Proposition 1 考虑一个DAG G并假设分布P忠诚于G。标记最大邻居数为 $q=max_{1≤j≤p}|adj(G, j)|$。然后,$PC_{pop}$ 算法构建DAG的真实骨架。此外,$m_{reach}∈{\lbrace q-1, q\rbrace}$

(原文附录A中给出了证明。)

2.2.2 SAMPLE VERSION FOR THE SKELETON

对于有限的样本,需要估计条件独立性。本文将自身限制在高斯情况下,其中所有节点都对应于具有多元正态分布的随机变量。此外,本文假设忠诚性模型,这意味着条件独立关系对应于d-separation (因此可以从图中读出),反之亦然;见第2.1节。

在高斯情况下,可以从偏相关性中推断出条件的独立性。

Proposition 2 假设随机变量X的分布P是多元正态分布。对于$i ≠ j\in {1,…, p}, k⊆{\lbrace 1,…, p\rbrace}\setminus {\lbrace i, j\rbrace}$,使用符号$\rho_{i,j\mid k}$来表示$X^{(i)}$和$X^{(j)}$在给定 $\lbrace X^{(r)};r\in k \rbrace$时的偏相关性。那么,当且仅当$X^{(i)}$和$X^{(j)}$在给定$\lbrace X^{(r)};r\in k \rbrace $条件独立时$\rho_{i,j\mid k}$的值为0。

证明:该主张是多变量正态分布的一个基本属性,见Lauritzen(1996, Prop. 5.2.)。

因此可以估计偏相关性,以获得条件独立性的估计。样本偏相关性$\hatρ$可以通过回归,部分协方差矩阵的反演,或者使用以下恒等式来递归计算:对于h∈k,

image-20221028195415811

本文使用的是递归方法(上式,即第三种方法)。为了检验偏相关系数是否为0,本文使用Fisher’s z-transform对偏相关系数进行转换。

image-20221028195600738

当使用显著性水平α时,经典决策理论生成了以下规则。$H_0$和$H_A$。当$sqrt(n−|k|−3|)Z(i,j|k)|>Φ^{−1}(1−α/2)$时,接受$H_A:\rho_{i,j|k}≠0$;拒绝$H_0:\rho_{i,j|k}=0$;其中Φ(·)代表N(0, 1)的累积分布函数。

用 $sqrt(n−|k|−3|)Z(i,j|k)| ≤ Φ^{−1}(1−α/2)$替换 $PC_{pop}$算法 中的第11行:“if i and j are conditionally independent given k then”。该算法产生一个依赖于数据的值$\hat{m}_{reach}$,它是公式(2)的样本版本。

$PC_{pop}$ 算法的唯一调优参数是α,这是检验偏相关性的显著性水平。

正如将在第3节中看到的,即使p远大于n,但当DAG是稀疏的,该算法也是渐进一致的。

2.3 Extending the Skeleton to the Equivalence Class

在寻找算法1中的骨架时,记录了使边缘在由S表示的变量中消失的分离集。这对于寻找骨架本身来说是不必要的,但对于将骨架扩展到等价类来说是必不可少的。在算法2中,描述了Pearl(2000, p.50f)的工作,将骨架扩展到属于底层DAG的等价类的CPDAG。

image-20221028200302479

算法2的输出是一个CPDAG,这是由Meek(1995b)首次证明的。

3. Consistency for High-Dimensional Data

由第二节可以看到,首先处理寻找骨架的问题。接下来,将结果扩展到寻找CPDAG。

3.1 Finding the Skeleton

本文将证明第2.2.2节中的PC算法对于DAG的骨架是渐进一致的,即使p远大于n,而DAG是稀疏的。本文假设数据是i.i.d.(独立同分布)的向量$X_1, …, X_n,X_i∈R^p$ 来自有对应分布P的DAG G。为了观察高维行为,本文将使维数作为样本大小的函数增长:因此,$p=p_n$,且$DAG~G=G_n$,分布$P = P_n$。假设如下:

  • (A1)对于任意的n,分布Pn是多元正态分布且忠诚于DAG $G_n$。
  • (A2)维数$p_n=O(n^a),0≤a<∞$
  • (A3)$DAG~G_n$中的最大邻居数表示为 $q_n=max_{1≤j≤p_n}|adj(G,j)|$,与$q_n=O(n^{(1−b)}), 0<b≤1$。
  • (A4)对于集合$k⊆{\lbrace1,…,p_n}\rbrace \setminus {\lbrace i, j\rbrace}$,给定{X^(r)^; r∈k}时,$X^{(i)}$和$X^{(j)}$之间的偏相关用 $ρ_{n;i,j|k}$ 表示。它们的绝对值有上界和下界:image-20221028200721136)(A1)是图模型中常用的假设,尽管它确实限制了可能的概率分布类型(见2.1节的第三段);(A2)允许维度作为样本量的函数的任意多项式增长,即高维度;(A3)是稀疏假设;(A4)是正则性条件。定理1如下图:image-20221028200803279

3.2 Extending the Skeleton to the Equivalence Class

如前所述,所有的推理都是在寻找骨架时完成的。如果这部分完美地完成,也就是说,如果在测试条件独立性时没有错误(假设骨架被正确估计是不够的),第二部分将永远不会失败(见Meek,1995b)。因此,很容易得到定理2:

image-20221028200900068

4. Numerical Examples

本文分析了PC算法使用各种模拟数据集来寻找骨架和CPDAG。利用R语言的pcalg包得到了数值结果。关于对不同算法的广泛的数值比较研究,文章参考了Tsamardinos等人的文章(2006)。

4.1 Simulating Data

4.2 Choice of Significance Level

文章中通过多组对比实验,认为α=0.005或α=0.01时的拟合效果最好。

4.3 Performance for Different Parameter Settings

为了保持概述在一个可管理的大小,文章将后续实验的显著性水平限制为α = 0.01。

4.4 Properties in High-Dimensional Setting

从某种意义上说,DAG的真实边的百分比的稀疏性是下降的,而在另一种意义上,邻域大小的稀疏性是随着n的增加而增加的。

4.5 Computational Complexity

PC 算法的计算复杂度难以精确计算,但最坏情况是以作为维数p的函数的公式(4)为界的。

5. R-Package pcalg

R语言包pcalg可用于从数据中估计DAG的底层骨架或等价类。若要使用此软件包,就需要安装统计软件R。R和R包都可以在http://www.r-project.org免费获得。对于低维问题(但不是对于成百成千上万的p),还有许多pc算法的其他实现也值得一提:

• Hugin at http://www.hugin.com .

• Murphy’s Bayes Network toolbox at http://bnt.sourceforge.net .

• Tetrad IV at http://www.phil.cmu.edu/projects/tetrad .

接下来将展示一个如何使用R包pcalg生成随机DAG,抽取样本并从数据中推断原始DAG的骨架和等价类。结果骨架和CPDAG中的边的线宽可以被调整,以对应于估计得到的依赖关系的可靠性。(线宽与✓(n−|k|−3)Z(i, j, k)的最小值成正比。因此,粗线是可靠的)。

示例代码(原文和新版本R包)

原文版本 Old example in this paper(the new version of R-package)

library(pcalg)## define parameters# number of random variablesp <- 10# number of samplesn <- 10000# sparsness of the graphs <- 0.4 ## generate random dataset.seed(42)# generate a random DAGg <- randomDAG(p, s) # generate random samplesd <- rmvDAG(n, g) ## Note : pcAlgo is DEPRECATED in the new version of pcalg package, and the new method 'pc', 'skeleton' is recommended.# estimate of the skeletongSkel <- pcAlgo(d, alpha=0.05) gCPDAG <- udag2cpdag(gSkel)# The CPDAG can also be estimated directly usinggCPDAG <- pcAlgo(d, alpha=0.05, directed=TRUE)## plot the graphplot(g)plot(gSkel, zvalue.lwd=TRUE)plot(gCPDAG, zvalue.lwd=TRUE)

image-20221028201108702

新的R包版本 New example in the new version of R-package

## Using Gaussian Datalibrary(pcalg)library(Rgraphviz)library(graph)library(BiocGenerics)library(grid)# Load predefined datadata(gmG)n <- nrow(gmG8$x)# labels aka node namesV <- colnames(gmG8$x) # estimate skeletonskel.fit <- skeleton(suffStat = list(C = cor(gmG8$x), n = n), indepTest = gaussCItest, alpha = 0.01, labels = V, verbose = TRUE)if (require(Rgraphviz)) {  ## show estimated Skeleton  par(mfrow=c(1,2))  plot(skel.fit, main = "Estimated Skeleton")  plot(gmG8$g, main = "True DAG")}# estimate CPDAGpc.fit <- pc(suffStat = list(C = cor(gmG8$x), n = n), indepTest = gaussCItest, alpha = 0.01, labels = V, verbose = TRUE)if (require(Rgraphviz)) {  ## show estimated CPDAG  par(mfrow=c(1,2))  plot(pc.fit, main = "Estimated CPDAG")  plot(gmG8$g, main = "True DAG")} 
image-20221028201156455image-20221028201240787

6. Conclusions

本文表明,对于DAG(由CPDAG表示)及其骨架的等价类,PC算法是渐进一致的,具有相应的高维稀疏高斯分布。此外,PC算法对于这种高维、稀疏的问题在计算上是可行的。把这两个事实放在一起,PC算法被确立为一种方法(到目前(文章发表于2007年)为止是唯一的一种),它在计算上是可行的,并且在统一一致性的意义上是可以证明正确的,适用于高维DAGs。稀疏性,即真正底层DAG的邻域的最大大小,对于统计一致性和计算可行性至关重要,其复杂度最多是作为维数函数的多项式。

文章强调,DAG的骨架经常提供有趣的见解,在高维环境下,使用无向的骨架作为更简单但更现实的目标而不是整个CPDAG是非常明智的。

LeetCode腾讯精选练习50题-011.盛水最多的容器

作者 whtli
2022年10月3日 11:00

题目描述

  • 给定一个长度为 n 的整数数组 height 。
  • 有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
  • 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
  • 返回容器可以储存的最大水量。
  • 说明:不能倾斜容器。
exampleinput  : [1,8,6,2,5,4,8,3,7]output : 49input  : [1,1]output : 1input  : [4, 4, 2, 11, 0, 11, 5, 11, 13, 8]output : 55

解题思路

  • 双指针+贪心

  • 双指针指向高度数组的首尾两端

  • 容器的面积取决于左右两指针之间的横向距离差左右两指针指向的数字中的较小值

  • 如果向内移动指向的数字较大的那个指针,那么前者左右两指针指向的数字中的较小值不会增加(会不变或者变小),后者左右两指针之间的横向距离差会减小,则两者乘积会减小

  • 因此,移动数字较大的那个指针是不合理的

  • 若想提高容量,能做的就是向内移动高度较小的指针(期望获得更高的高度),并比较移动后的容量是否大于当前最大容量

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

代码(Java)

public class Solution {    public int maxArea(int[] height) {        int left = 0;        int right = height.length - 1;        int maxArea = 0;        while (left < right) {            int area = (right - left) * Math.min(height[left], height[right]);            maxArea = Math.max(area, maxArea);            if (height[left] >= height[right]) {                right--;            } else {                left++;            }        }        return maxArea;    }}

LeetCode腾讯精选练习50题-061.旋转链表

作者 whtli
2022年6月5日 21:00

题目描述

  • 给定一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
exampleinput  : head = [1,2,3,4,5], k = 2output : [4,5,1,2,3]input  : head = [0,1,2], k = 4output : [2,0,1]

解题思路

  • 如果满足以下条件,可以直接返回原链表
    • 链表为空,即 head = null
    • 链表只有一个节点,即 head.next = null
    • 旋转次数 k 为0
    • 旋转次数 k 是链表长度的倍数,即旋转后的链表还是原样
思路1 双指针/快慢指针
  • 第一次遍历获取链表长度

  • 用快慢指针拉开遍历差距,并借助慢指针获取结果链表的起始节点,即

    • 先考虑简单情况 k 严格小于 链表长度 n,那么就是要找到原链表的倒数第k个节点作为结果链表的起始节点
    • 再拓展到k > n的情况,此时只需要刷新k,即令 k = k % n,转变成第一种简单情况即可
  • 使链表闭合,并断开新的起始节点与其之前节点的链接

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

    思路2 双指针的反向思考
  • 其实就是双指针思路的变形

  • 第一次遍历获取链表长

  • 反向思考旋转后结果链表起始节点的位置,使用单个指针遍历来获取结果链表的起始节点,即

    • 先考虑简单情况 k 严格小于 链表长度 n,那么就是要找到原链表的倒数第k个节点原始链表的正数第 n - k 个节点的后继结点作为结果链表的起始节点
    • 再拓展到k > n 的情况,此时只需要将 k % n,转变成第一种简单情况即可,即刷新k,令 k = n - k % n
  • 使链表闭合,并断开新的起始节点与其之前节点的链接

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

代码(Java)

思路1代码

public class Solution1 {    public ListNode rotateRight(ListNode head, int k) {        if (k == 0 || head == null || head.next == null) {            return head;        }        ListNode iter = head;        int len = 0;        ListNode tail = null;        while (iter != null) {            if (iter.next == null) {                tail = iter;            }            len++;            iter = iter.next;        }        // 如果旋转次数恰好是链表长度的倍数,说明旋转后也是原样,所以不需要移动        if (k % len == 0) {            return head;        }        // 初始令计数器延后一位(即-1),以获取结果链表的起始位置的前一个位置        // 此处若初始令计数器为0,则获得的是结果链表的其实位置,无法将它与之前的节点断开        int cnt = -1;        // 先令快指针向前遍历k%len步        ListNode fast = head;        while (cnt != k % len) {            cnt++;            fast = fast.next;        }        // 再同步开启慢指针的遍历        // 当快指针为空时,说明慢指针已经到达了目标结果链表的起始节点的前一个位置        ListNode second = head;        while (fast != null) {            fast = fast.next;            second = second.next;        }        // 使原始链表闭合为环        tail.next = head;        // 获得新起点        ListNode res = second.next;        // 从新起点之前断开        second.next = null;        // 返回结果        return res;    }}

思路2代码

public class Solution2 {    public ListNode rotateRight2(ListNode head, int k) {        if (k == 0 || head == null || head.next == null) {            return head;        }        ListNode first = head;        int len = 1;        while (first.next != null) {            len++;            first = first.next;        }        if (k % len == 0) {            return head;        }        // 第二次遍历        ListNode second = head;        int cnt = 1;        // 移动k个位置,其实就是以 第len - k % len个节点的后继节点 作为新链表的头节点,并使其与其之前的节点断开链接        // 所以此处可以做减法来替代快慢指针的同步遍历,只需要遍历一次即可。        int step = len - k % len;        while (cnt < step) {            cnt++;            second = second.next;        }        // 使之闭合为环        first.next = head;        ListNode res = second.next;        second.next = null;        return res;    }}

LeetCode腾讯精选练习50题-007.整数反转

作者 whtli
2022年5月28日 17:00

题目描述

  • 给定一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
  • 如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^(31 − 1)] ,就返回 0。
  • 假设环境不允许存储 64 位整数(有符号或无符号)。
exampleinput  : x = -123output : -321input  : x = 120output : 21input  : x = -2147483648output : 0input  : x = 1534236469output : 0

解题思路

  • 因为输入数据保证在 32 位的有符号整数的范围内,所以不需要考虑像 8463847412 这种超出本身就超出范围且反转后依然超出范围的情况

    思路1 字符串
  • 借助字符串str接收 x ,然后反转字符串str,并判断输入 x 的正负性、str与 “2147483648”、”-2147483648”的大小关系

    • 若x为正(+),且字符串str大于字符串”2147483647”,超出上界,返回0
    • 若x为负(-),且字符串str大于字符串”2147483648”,超出下界,返回0
    • 正常则取其Integer值并添上符号返回
  • 时间复杂度:O(n)

  • 空间复杂度:O(n),使用了额外的等“长”字符串

    思路2 让步比大小
  • “让步”的意思是在数值超出32位整形范围之前就把它判断出来,即用最大值/10和最小值/10的范围作为临时上下界来防止完全反转后的越界

  • 先统一转换成正数

    • 用整形变量res(初始值为0)不断扩大十倍并加上当前x%10得到的数,然后将x/10去掉它的末尾数值
    • 在执行以上操作之前,先判断一下当前的res是否大于32位整型变量的最大值Integer.MAX_VALUE/10,若是则说明完全的反转后肯定会越界,不必继续操作,直接按要求返回0即可
    • 当x减小到0的时候,判断原始的符号位,并给结果res添加上,然后返回
  • 不转换正数,统一判断

    • 用整形变量res(初始值为0)不断扩大十倍并加上当前x%10得到的数,然后将x/10去掉它的末尾数值
    • 在执行以上操作之前,先判断一下以下两条,若满足则说明完全的反转后肯定会越界,不必继续操作,直接按要求返回0即可
      • 当前的res是否大于32位整型变量的最大值Integer.MAX_VALUE / 10
      • 当前的res是否小于32位整型变量的最小值Integer.MIN_VALUE / 10
    • 当x等于0的时候,返回res即可(不需要在判断符号位了)
  • 时间复杂度:O(log(n))

  • 空间复杂度:O(1),只使用了有限个整型变量

代码(Java)

思路1代码

public class Solution1 {    public int reverse(int x) {        if (x == Integer.MIN_VALUE){            // 因为输入保证为32位整数,所以不需要考虑更小的数值,判断到Integer的最小值即可            return 0;        }        boolean flag = false;        if (x < 0) {            flag = true;            x = -x;        }        StringBuilder s = new StringBuilder(x + "");        String str = s.reverse().toString();        if (s.length() >= 10) {            if (!flag && str.compareTo("2147483647") > 0) {                return 0;            }            if (flag && str.compareTo("2147483648") > 0) {                return 0;            }        }        return flag ? -Integer.parseInt(str) : Integer.parseInt(str);    }}

思路2代码

public class Solution2 {    public int reverse(int x) {        if (x < 0 && x == -x) {            return 0;        }        boolean flag = false;        if (x < 0) {            flag = true;            x = -x;        }        int y = 0;        while (x > 0) {            if (y > Integer.MAX_VALUE / 10) {                return 0;            }            y = y * 10 + x % 10;            x /= 10;        }        return flag ? -y : y;    }    // 以下为简化代码    public int reverse2(int x) {        int y = 0;        int max = Integer.MAX_VALUE / 10;        int min = Integer.MIN_VALUE / 10;        while (x != 0) {            if (y > max || y < min) {                return 0;            }            y = y * 10 + x % 10;            x /= 10;        }        return y;    }}

LeetCode腾讯精选练习50题-070.爬楼梯

作者 whtli
2022年5月20日 21:00

题目描述

exampleinput  : n = 2output : 2note   : 有两种方法可以爬到楼顶。         1. 1 阶 + 12. 2 阶input  : n = 3output : 3note   : 有三种方法可以爬到楼顶。         1. 1 阶 + 1 阶 + 12. 1 阶 + 23. 2 阶 + 1

解题思路

  • 列举几个简单的台阶数,做总结,可以发现爬楼梯的方案数满足斐波那契数列。如:

    • 2阶,2种
    • 3阶,3种
    • 4阶,5种
    • 5阶,8种
      思路1 动态规划
  • 状态转移方程:dp[i] = dp[i-1] + dp[i-2]

  • 初始,dp[1] = 1, dp[2] = 2

  • 从3到n遍历结束后,返回dp[n]即是爬楼梯的方案数

  • 可以维护dp数组,保留每一个n可以产生的方案数

  • 也可以不维护数组,动过迭代变量保留最后的方案数

思路2 递归(会超出时间限制)
  • 从动态规划变形得到的
  • 递归出口:
    • n = 1,返回1
    • n = 2,返回2
  • 递归过程:
    • 返回 loop(n - 1) + loop(n - 2)
思路3 数学通项公式
  • 斐波那契数列的通项公式为:
    • f(n)=[((1+√5) / 2)^n^−((1−√5)/2)^n^] / √5

代码(Java)

思路1代码

public class Solution1 {    public int climbStairs(int n) {        if (n == 1){            return 1;        }        int[] dp = new int[n + 1];        dp[1] = 1;        dp[2] = 2;        for (int i = 3; i <= n; i++){            dp[i] = dp[i-1] + dp[i-2];        }        return dp[n];    }    // 不借助dp数组的写法    public int climbStairs2(int n){        int p = 0;        int q = 1;        int r = p + q;        for (int i = 2; i <= n; i++){            p = q;            q = r;            r = p + q;        }        return r;    }}

思路2代码

public class Solution2 {    public int climbStairs(int n) {        // 超出时间限制O(2^n)        if (n <= 1) {            return 1;        }        return climbStairs(n - 1) + climbStairs(n - 2);    }}

思路3代码

public class Solution3 {    public int climbStairs(int n) {        double sqrt5 = Math.sqrt(5);        double fibn = Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2, n + 1);        return (int) Math.round(fibn / sqrt5);    }}

LeetCode腾讯精选练习50题-155.最小栈

作者 whtli
2022年5月16日 21:00

题目描述

  • 设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
  • 实现 MinStack 类:
  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素。
  • int getMin() 获取堆栈中的最小元素。
exampleinput  : ["MinStack","push","push","push","getMin","pop","top","getMin"]         [[],[-2],[0],[-3],[],[],[],[]]output : [null,null,null,null,-3,null,0,-2]note   : MinStack minStack = new MinStack();         minStack.push(-2);         minStack.push(0);         minStack.push(-3);         minStack.getMin();   --> 返回 -3.         minStack.pop();         minStack.top();      --> 返回 0.         minStack.getMin();   --> 返回 -2.

解题思路

思路1 维护最小值栈
  • 每次在元素栈中入栈一个元素之后,就在最小值栈中入栈一个当前元素栈中的最小值

  • 这样就可以在pop过程中同步变更两个栈,来保证每次geiMin获取的最小值都是当前元素栈中的最小值

  • MinStack()

    • 初始化元素栈 elem 、最小值栈 min
    • 最小值栈 min 中先入栈Integer.MAX_VALUE;
  • void push(int val)

    • elem 中入栈 val
    • 比较 min 的栈顶元素和 val 的大小,把小的入 min 栈
  • void pop()

    • elem 栈顶元素出栈
    • min 栈顶元素也出栈
  • int top()

    • 获取 elem 栈顶元素值,但是栈顶元素不出栈
  • int getMin()

    • 获取 min 栈顶元素值,但是栈顶元素不出栈
  • 时间复杂度O(1)

  • 空间复杂度O(n)

    思路2 以数组为元素
  • 只维护一个元素栈,但栈中元素是数组,每次入栈的数组的形式为 [当前需要入栈的元素val, 当前栈顶数组元素中的第2个元素 和 当前元素val 中的较小者]

  • 这样就相当于把思路1中的两个栈,维护到了一个栈中,把元素值和当前最小值放到数组中整体入栈

  • MinStack()

    • 初始化元素栈 elem<int[]>
  • void push(int val)

    • 通过比较val 和 栈顶数组的第 2 个元素 得到 较小值 min
    • elem 中入栈 [val, min],即 elem.push([val, Math.min(val, elem.peek()[1])])
  • void pop()

    • elem 栈顶数组出栈
  • int top()

    • 获取 elem 栈顶数组中的第 1 个元素,但是栈顶数组不出栈
  • int getMin()

    • 获取 elem 栈顶数组中的第 2 个元素,但是栈顶元素不出栈
  • 时间复杂度O(1)

  • 空间复杂度O(n)

    代码(Java)

思路1代码

public class MinStack1 {    Stack<Integer> stack;    Stack<Integer> min;    public MinStack1() {        stack = new Stack<>();        min = new Stack<>();        min.push(Integer.MAX_VALUE);    }    public void push(int val) {        stack.push(val);        if (val < min.peek()) {            min.push(val);        } else {            min.push(min.peek());        }    }    public void pop() {        stack.pop();        min.pop();    }    public int top() {        return stack.peek();    }    public int getMin() {        return min.peek();    }}

思路2代码

public class MinStack2 {    Stack<int[]> stack;    public MinStack2() {        stack = new Stack<>();    }    public void push(int val) {        if (stack.empty()){            stack.push(new int[]{val, val});        } else {            stack.push(new int[]{val, Math.min(val, stack.peek()[1])});        }    }    public void pop() {        stack.pop();    }    public int top() {        return stack.peek()[0];    }    public int getMin() {        return stack.peek()[1];    }}

思路2代码 非数组版

public class MinStack3 {    Stack<Integer> stack;    public MinStack3() {        stack = new Stack<>();    }    public void push(int val) {        if (stack.empty()){            stack.push(val);            stack.push(val);        } else {            int min = stack.peek();            stack.push(val);            stack.push(Math.min(val, min));        }    }    public void pop() {        stack.pop();        stack.pop();    }    public int top() {        int min = stack.pop();        int top = stack.peek();        stack.push(min);        return top;    }    public int getMin() {        return stack.peek();    }}
❌
❌