Leetcode | 947. Most Stones Removed with Same Row or Column [Union-Find/DPS]

本文深入解析LeetCode上的Most Stones Removed with Same Row or Column问题,介绍了一种利用并查集(Union-Find)解决连通图问题的方法。通过具体代码实现,展示了如何计算在一个二维平面上,最多可以移除多少个石头,使得任意两个石头不在同一行或同一列。文章重点讨论了字符串索引的使用,避免了传统1D索引转换的复杂性,并强调了权重在并查集中的作用。

Problem

https://leetcode.com/problems/most-stones-removed-with-same-row-or-column/

分析

https://leetcode.com/problems/most-stones-removed-with-same-row-or-column/discuss/197668/Count-the-Number-of-Islands-O(N)

本质是个连通图问题,最大移动石头的个数 = total - islands

解法:Union-Find 或者 DFS

注意:

  • 题目有ij的范围以及stones长度范围,需要注意空间使用大小,2D转1D的索引范围过大,存储上考虑用HashMap代替数组;
  • String作为key是一个新思路,避免了每次转换1D;
  • 需要使用权重来避免UF树过高

解法

public class MostStonesRemovedWithSameRowOrColumn {
    private final Map<Integer, String> rows = new HashMap<>();
    private final Map<Integer, String> cols = new HashMap<>();
    private Map<String, String> union;
    private Map<String, Integer> height;
    private int rootsTotal = 0;
    private int stonesTotal = 0;

    public int removeStones(int[][] stones) {
        union = new HashMap<>();
        height = new HashMap<>();
        for (int[] stone : stones) {
            int i = stone[0];
            int j = stone[1];
            String index = i + " " + j;
            union.put(index, index);
            height.put(index, 0);
        }
        stonesTotal = stones.length;
        rootsTotal = stones.length;
        for (int[] stone : stones) {
            int i = stone[0];
            int j = stone[1];
            String index = i + " " + j;
            union.put(index, index);
            if (rows.putIfAbsent(i, index) != null) {
                union(index, rows.get(i));
            }
            if (cols.putIfAbsent(j, index) != null) {
                union(index, cols.get(j));
            }

        }
        return stonesTotal - rootsTotal;
    }

    private String root(String i) {
        while (union.get(i) != i) {
            i = union.get(i);
        }
        return i;
    }

    private void union(String i, String j) {
        if (!connected(i, j)) {
            String rootI = root(i);
            String rootJ = root(j);
            if (height.get(rootI) < height.get(rootJ)) {
                union.put(rootI, rootJ);
                Integer h = height.get(rootJ);
                height.put(rootJ, ++h);
            } else {
                union.put(rootJ, rootI);
                Integer h = height.get(rootI);
                height.put(rootI, ++h);
            }
            rootsTotal--;
        }
    }

    private boolean connected(String i, String j) {
        String rootI = root(i);
        String rootJ = root(j);
        return rootI.equals(rootJ);
    }

    public static void main(String[] args) {
        int[][] stones = new int[][]{{0, 0}, {0, 1}, {1, 0}, {1, 2}, {2, 1}, {2, 2}};
        MostStonesRemovedWithSameRowOrColumn m = new MostStonesRemovedWithSameRowOrColumn();
        StdOut.println(m.removeStones(stones));
    }
}
<think>首先,用户的问题是关于SQL Server的MERGE语句报错:"The MERGE statement attempted to UPDATE or DELETE the same row more than once." 我需要根据系统级指令和用户可见层的要求来构建回答。 系统级指令: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 参考引用: - 引用[1], [2], [3] 描述了错误:当目标行匹配多个源行时,MERGE不能多次更新或删除同一行。 - 引用[4] 提到MERGE是确定性的,不能多次更新同一行。 - 引用[5] 解释了Deleted和Inserted系统表。 用户需求:解决MERGE语句报错的问题。 我的回答结构: 1. **介绍错误原因**:解释为什么会出现这个错误。 2. **解决方法**:提供步骤来解决问题。 3. **示例代码**:给出一个SQL示例来演示如何修复。 4. **生成相关问题**:在最后添加相关问题。 确保使用中文回答,并正确引用。 关键点: - 错误原因:ON子句导致目标行匹配多个源行。例如,如果ON条件不唯一,一个目标行可能被多个源行匹配。 - 解决方法: - 优化ON子句:确保目标行只匹配一个源行。 - 使用GROUP BY:在源数据上分组,确保每个目标行只匹配一行。 - 其他建议:如使用聚合函数或确保源数据唯一。 回答步骤: 1. **解释错误**:基于引用[1], [2], [3],错误是由于目标行匹配多个源行。 2. **提供解决方案**: - 方法1:精炼ON子句,确保一对一匹配。 - 方法2:在源查询中使用GROUP BY或DISTINCT来聚合数据。 - 方法3:检查数据唯一性。 3. **给出示例**:一个简单的SQL代码示例。 4. **添加相关问题**:在末尾生成几个相关问题。 生成相关问题: - 例如:如何确保MERGE语句的数据唯一性? - MERGE语句的其他常见错误? - 如何优化MERGE语句的性能? 最后,确保回答结构清晰:分步骤,用标题或编号。 完整回答大纲: - 开头:简述错误和原因。 - 主体:详细解释解决方法。 - 代码块:示例SQL。 - 结尾:相关问题。</think>### SQL Server MERGE语句报错:The MERGE statement attempted to UPDATE or DELETE the same row more than once. 的解决方法 在使用SQL Server的MERGE语句时,如果出现错误“The MERGE statement attempted to UPDATE or DELETE the same row more than once”,这通常是由于目标表中的一行匹配了源数据中的多行导致的。MERGE语句要求每个目标行只能被更新或删除一次,否则会违反其确定性原则[^4]。下面我将逐步解释错误原因并提供解决方案,确保您的MERGE操作顺利执行。 #### 错误原因分析 - **核心问题**:当ON子句的匹配条件不唯一时,目标表中的一行可能与源数据中的多行匹配。这会导致MERGE尝试对同一目标行执行多次UPDATE或DELETE操作,从而触发错误[^1][^2][^3]。 - **示例场景**:假设目标表`TargetTable`有一个主键`ID`,而源数据`SourceTable`中有多行具有相同的`ID`值。如果ON子句基于`ID`匹配,则一个目标行会匹配多个源行,引发错误。 - **系统行为**:MERGE语句在执行时,会使用Deleted和Inserted系统表来跟踪变更;但一次操作中,同一目标行不能被多次修改[^5]。 #### 解决步骤 要解决此错误,您需要确保目标行最多只匹配一个源行。以下是三种可靠的方法,按推荐顺序排列: 1. **精炼ON子句,确保唯一匹配** 修改ON子句的条件,使其能唯一标识目标行。通常,这需要基于主键或唯一约束。 - **操作步骤**: - 检查目标表和源数据的键是否唯一(如使用`SELECT DISTINCT`)。 - 在ON子句中添加更多条件(如时间戳或状态列),使匹配更精确。 - 例如:如果目标表有主键`ID`和`Version`列,ON子句应包含两者。 - **优点**:直接解决问题,无需额外聚合。 - **注意事项**:如果源数据本身有重复,需先清理。 2. **在源查询中使用GROUP BY聚合数据** 如果源数据有重复行,使用GROUP BY或聚合函数(如`MAX`或`SUM`)在源查询中合并数据,确保每个目标行只匹配一行。 - **操作步骤**: - 在MERGE的源部分(USING子句)中,添加GROUP BY对关键列分组。 - 选择聚合函数处理非键列(如取最新值)。 - 示例代码: ```sql MERGE INTO TargetTable AS Target USING ( SELECT ID, MAX(Value) AS AggValue -- 使用MAX聚合重复值 FROM SourceTable GROUP BY ID -- 确保每个ID只输出一行 ) AS Source ON Target.ID = Source.ID -- ON子句基于唯一ID WHEN MATCHED THEN UPDATE SET Target.Value = Source.AggValue WHEN NOT MATCHED BY TARGET THEN INSERT (ID, Value) VALUES (Source.ID, Source.AggValue); ``` - 如果执行此代码后错误消失,说明源数据重复是根本原因[^2][^3]。 3. **检查数据唯一性并预处理源数据** 在运行MERGE前,验证源数据和目标表的数据唯一性。 - **操作步骤**: - 运行查询检查重复:`SELECT ID, COUNT(*) FROM SourceTable GROUP BY ID HAVING COUNT(*) > 1`。 - 如果发现重复,使用临时表或CTE清理源数据(如删除重复行或添加唯一索引)。 - 在ON子句中添加唯一约束列(如`ROW_NUMBER()`)。 - **优点**:预防性强,适用于复杂场景。 #### 注意事项 - **性能影响**:GROUP BY方法可能增加查询开销;如果源数据量大,优先优化ON子句。 - **测试建议**:在生产环境前,在测试环境运行MERGE并检查Deleted和Inserted系统表的变化[^5]。 - **常见错误来源**:源数据导入错误(如CSV文件重复行)或JOIN逻辑缺陷。定期审核数据质量可减少此类问题。 通过以上方法,您可以有效解决MERGE语句的报错。如果问题持续,请提供具体SQL代码和表结构,以便进一步诊断。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值