【周赛总结】并查集筛法,排序,字符串操作与gcd,线段树与逆元,dp压缩

1627. 带阈值的图连通性(并查集,筛法)

在这里插入图片描述在这里插入图片描述

首先这道题很显然是可以用并查集解决的,尤其是查找的工作。复杂度主要卡在构造连通图上。我们可以使用类似求素数的埃氏筛的方法。我们去枚举公因数,最小一定是从threshold开始,最大应该是n。然后在枚举可能公因数是这数字的两个数字。最小的肯定是a = x, b = 2*x, 然后依次增加x。保证找全这个公因数所有的倍数并且联通。

class Solution {
    public List<Boolean> areConnected(int n, int threshold, int[][] queries) {
        UF uf = new UF(n+1);
        List<Boolean> ans = new ArrayList<>();
        for (int z = threshold + 1; z <= n; ++z) {
            // 枚举两个 z 的倍数的点并连接
            for (int p = z, q = z * 2; q <= n; p += z, q += z) {
                uf.union(p, q);
            }
        }
        for (int[] q:queries){
            if (uf.isUnion(q[0],q[1])) ans.add(true);
            else ans.add(false);
        }
        return ans;
    }

    private int gcd(int x, int y){
        return y == 0? x: gcd(y, x %y);
    }

class UF{
    int[] fa;
    int[] sz;
    public UF(int N){
        fa = new int[N];
        sz = new int[N];
        for (int i = 0; i<N; i++)fa[i] = i;
        for (int i = 0; i<N; i++)sz[i] = 1;
    } 

    public int findfa(int x){
        if (x == fa[x])return x;
        fa[x] = findfa(fa[x]);
        return fa[x];
    }

    public boolean isUnion(int x, int y){
        return findfa(x) == findfa(y);
    }

    public void union(int x, int y){
        if (isUnion(x, y)) return ;
        int xfa = findfa(x);
        int yfa = findfa(y);
        if (sz[xfa]<sz[yfa]){
            fa[xfa] = yfa;
            sz[yfa] += sz[xfa];
        }else {
            fa[yfa] = xfa;
            sz[xfa] += sz[yfa];
        }
        return;
    }
}

}

如果继续优化的话可以使用埃氏筛等。

1626. 无矛盾的最佳球队(lambda排序)

在这里插入图片描述

这道题还是没有想到使用dp的方法。维护了dp[i]表示选取该队员时,和前面的所有人组合能够得到的最大得分。我们对其联合排序,年龄小的在前,相同年龄的话,得分高的在后。

这里有一个新东西,是用Java实现多因素排序的方法。这个在python中是很简单的但是Java中需要调用Comparator<?>接口并且重写compare方法。

这里给出两个方法实现,其中lambda表达式会更方便

class Solution {
    public int bestTeamScore(int[] scores, int[] ages) {
        if(scores.length == 1)
            return scores[0];
        // 联合排序的方法    
        int[][] arr = new int[scores.length][2];
        for(int i = 0; i < scores.length; i++){
            arr[i][0] = ages[i];
            arr[i][1] = scores[i];
        }
        // lambda 表达式的方法。
        //===============================
        Arrays.sort(arr, (a,b)-> a[0] == b[0]?a[1]-b[1]:a[0]-b[0]);
        //  = 重写接口的方法======================
        Arrays.sort(arr, new Comparator<int[]>(){ 
        // 注意拼写,Comparator  需要重写的方法是compare
            //@override
            public int compare(int[] a, int[] b){
                if (a[0] == b[0]) return a[1]-b[1];
                return a[0]-b[0];
            }
        });
        //=========================
        int[] dp = new int[scores.length];
        int ans = 0;
        for(int i = 0; i < scores.length; i++) {
            dp[i] = arr[i][1];
            for(int j = 0; j < i; j++) { // 找在i之前有没有比i小的
                if(arr[j][1] <= arr[i][1]) {
                    dp[i] = Math.max(dp[j]+arr[i][1], dp[i]);
                }
                ans = Math.max(ans,dp[i]);
            }
        }
        return ans;
    }
}

1625. 执行操作后字典序最小的字符串(最大公因数,字符串转换)

在这里插入图片描述

这道题真的有点难的,需要用枚举的方法。但是这里有一个小技巧可以记住:对于长度为n的数列,进行长度为m的轮转,等价于进行长度为两者最大公约数即gcd(m,n)的轮转

同时,利用这道题,也可也总结先java中的与字符串相关的各种转换。

  • String s-> char[] p : p = s.toCharArray();
  • char[] p -> String s : s = String.valueOf ( p);
  • int x -> String s : s = String.valueOf(x); 或者 s = Integer.toString(x);
  • String s -> int x : x = Integer.valueOf(s)
  • StringBuffer sb -> String s : s = sb.toString();
  • String s -> StringBuffer sb : sb = new StringBuffer(s);

整个题目的思路在于,首先枚举轮转,这里很巧妙的采用了两个字符串拼接在一起,这样可以得到任意一种开头的方法,然后只需要进行相应的截取就可以。

然后枚举增加,这里需要注意,因为题目给定了字符串的长度一定是偶数,因此我们需要考虑,如果轮转也是偶数的话是没法改变偶数位置的。

最后一个我们再完成累加。

另外有一个板子的方法,求解两个数的最大公因数,是用的辗转相除法。

!! 求最大公因数,背会,真的是不断的交换位置辗转相除。
    private int gcd(int x, int y) {
        return y == 0 ? x : gcd(y, x % y);
    }
class Solution {

// String s-> char[] p : p = s.toCharArray();
// char[] p -> String s : s = String.valueOf(p);
// int x -> String s : s = String.valueOf(x); 或者 s = Integer.toString(x);
// String s -> int x : x = Integer.valueOf(s)
// StringBuffer sb -> String s : s = sb.toString();
// String s -> StringBuffer sb : sb = new StringBuffer(s);

    // 最大公因数的经典求法
    private int gcd(int x, int y) {
        return y == 0 ? x : gcd(y, x % y);
    }
    // 这里进行轮换的时候,只需要对最大公因数进行操作即可

    public String findLexSmallestString(String s, int a, int b) {
        int n = s.length();
        String ans = s;
        String t = s + s;
        int g = gcd(n, b);
        for (int i = 0; i < n; i += g) {
            String p = t.substring(i, i+n);
            for (int j = 0; j <= 9; ++j) {
                int th = g % 2 == 0 ? 0 : 9;
                for (int k = 0; k <= th; ++k) {
                    char[] q = p.toCharArray();
                    for (int tt = 1; tt < n; tt += 2)
                        q[tt] = (char)('0' + (q[tt] - '0' + a * j) % 10);
                    // 如果是偶数的话,这里k=0
                    for (int tt = 0; tt < n; tt += 2)
                        q[tt] = (char)('0' + (q[tt] - '0' + a * k) % 10);
                    //
                    String now = String.valueOf(q);
                    // Java中比较字典的序的方法,调用compareTo这个实例方法,如果now的字典序大于ans,返回一个正数
                    ans = now.compareTo(ans)>0?ans:now;
                }
            }
        }
        return ans;
    }
}

1622. 奇妙序列(线段树,逆元)

在这里插入图片描述

是一道线段树的板子题,但是我还没完全搞定线段树,这里先挖个坑吧。先说一下我目前的方法。从题目可以看出,我们最后返回的值其实可以用一个表达式得到。Y = Ax+B。我们需要维护三个数列,一个是数值,一个是这个数值在加入时候的A的值,一个是加入进来的B的值。

在求解的时候,对于index位置的数字,其实在数字加入之后的操作都是有效的但是之前的操作都是他享受不到的。已知 A, B, A1, B1, 求解A2,B2,也就是在加入之后的表达式的系数。

Ax +B = A2(A1x+B1)+B2
得到:
A = A1 * A2
B = B1 * A2 + B2
求得:
A2 = A * invA1
B2 = B-B1 * A * invA1

需要注意这里的inv A1表示逆元,又是一个很板子的方法。

求解逆元: a − 1 = a m − 2 m o d ( m ) a^{-1} = a^{m-2}mod(m) a1=am2mod(m) a的逆元等于 a m − 2 a^{m-2} am2对m取模的结果。要求a,m互质。

费马小定理: a b ≡ a b m o d    ϕ ( b ) m o d    b a^b \equiv a^{b\mod{\phi(b)}} \mod b ababmodϕ(b)modb,其中 ϕ ( b ) \phi(b) ϕ(b)是欧拉函数,对于质数,欧拉函数为b-1。这个方法可以用来降低幂次。

class Fancy:

    def __init__(self):
        self.mod = 10**9 + 7
        self.v = []
        self.mul = []
        self.add = []
        self.a = 1
        self.b = 0
    
    # 乘法逆元
    def inv(self, x):
        return pow(x, self.mod-2, self.mod)
    def append(self, val):
        self.v.append(val)
        self.add.append(self.b)
        self.mul.append(self.a)
    def addAll(self, inc):
        self.b = (self.b + inc) % self.mod
    def multAll(self, m):
        self.a = self.a * m % self.mod
        self.b = self.b * m % self.mod
    def getIndex(self, idx):
        if idx >= len(self.v):
            return -1
        a = self.a
        b = self.b
        a1 = self.mul[idx]
        b1 = self.add[idx]
        inva1 = self.inv(a1)
        a2 = a*inva1
        b2 = b-b1*a*inva1
        return (a2 * self.v[idx] + b2) % self.mod

1621. 大小为 K 的不重叠线段的数目(dp压缩)

在这里插入图片描述
在这里插入图片描述

动态规划的题目,但是如何优化是关键,首先我们很容易想到子问题是,给定 i段线段,要求得到 j段线段覆盖。也就是dp[i][j],我们可以想对于这个问题其实可以转化为 ,对于第一段,我们可以选也可以不选,也就是dp[n][k] = dp[n-1][k](不选)+dp[n-1][k-1](选) 但是这样是错误的,因为这里每天线段是可以跨越多个的,我们没有考虑第一条线其实可以覆盖从开头到之后很多区间的情况。

换个更好理解的思路,我们枚举以第一个点为起点的这条线的情况,

  1. 不存在以第一个点为起点的线,dp[n-1][k]
  2. 覆盖一个区间,剩下的k-1条去在n-1个空闲发挥,dp[n-1][k-1]
  3. 覆盖两个区间,剩下的k-1条去在n-2个空闲发挥,dp[n-2][k-1]
  4. 最终至少有,dp[k-1][k-1]

所以!dp[n][k] = dp[n-1][k]+dp[n-1][k-1]+dp[n-2][k-1]+...+dp[k-1][k-1]。注意dp的复杂度实在是很高的,因此我们要考虑再写一个,去找规律。dp[n-1][k] = dp[n-2][k]+dp[n-2][k-1]+...+dp[k-1][k-1]。这样可以看出dp[n][k] = 2*dp[n-1][k]+dp[n-1][k-1]-dp[n-2][k]

考虑初始化,肯定dp[i][i] = 1, 但是还需要dp[i][0]也为一,表示在长度为n的线段中,被0个线段的情况也是唯一的。

这里又涉及到了大数的问题,我们还是一样的操作。把int转换为long其实只需要在开头乘1L就可以。同时,由于存在减法,我们需要加上一个mod进行补偿。

class Solution {
    public int numberOfSets(int n, int k) {
        int[][] dp = new int[n+1][n+1];
        int mod = 1000000007;
        for (int i = 0; i<n+1; i++) {
            dp[i][i] = 1;
            dp[i][0] = 1;
        }

        for (int i = 2; i<n+1; i++){
            for(int j = 1; j<i; j++){
                dp[i][j] = (int)((2L*dp[i-1][j]+dp[i-1][j-1]-dp[i-2][j]+mod) % mod); 
            }
        }
        return dp[n-1][k];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值