【算法练习】日常刷题记录(贪心、SPFA改编、状态压缩、状态压缩DP)

一、最优除法(中等)

在这里插入图片描述
考虑数组中每个元素大小都>0,且对于最普通,不添加括号的情况,第0个数一定是分子,而第1个数一定是分母,但后面的数不一定,那我们只需要让分子足够大即可,那就是让第1个数后面的数全部成为分子。

class Solution {
     public String optimalDivision(int[] nums) {
          StringBuilder sb = new StringBuilder();
          // 遍历数组中元素
          for (int i = 0; i < nums.length; i++) {
               sb.append(nums[i]);
               // 后面紧跟除号,最后一个元素除外
               if (i + 1 < nums.length) {
                    sb.append('/');
               }
          }
          if (nums.length > 2) {
               sb.insert(sb.indexOf("/") + 1, '(');
               sb.append(')');
          }
          return sb.toString();
     }
}

二、K站中转内最便宜的航班(中等)

在这里插入图片描述
看似最短路径问题,需要在SPFA模板上做些改动,首先是中转站k站的限制,给出下面的示例:
在这里插入图片描述
k限制为1,刚开始0站点入队,第一次BFS遍历,1、2站点入队, k–,k=0,还可以继续入队,此时由于之前站点2出队了,而站点1又扩散到站点2,由于SPFA会对起点0到站点2的最短路径更新为2,此时站点2又扩散到终点3,此时得到的0 - 3的最短路径=3,但实际上=3的这条路径根本无法到达站点3(因为k=1),答案应该是6。

避免上述问题很简单,我们不使用vis数组,我们用队列记录每一条路径的信息:终点、花费和中转站次数。 这样从0-1-2的路径产生的起点到站点2的最短路径,不会影响从0-2的路径产生的最短路径,因为它们都是用node节点单独存储路径长度的,不是只存储最短路径。

不使用vis数组是为了能够实现路径的多次记录(因为可能单个结点多次出现在不同路径),之前SPFA使用vis是避免重复找最短路径,但本题不是找普通的最短路径,是在有限制情况下的最短路径。

class node {
    int curId;
    int cost;
    int step;
    node(int curId, int cost, int step) {
        this.step = step;
        this.cost = cost;
        this.curId = curId;
    }
}
class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        // 建图
        LinkedList<int[]>[] graph = new LinkedList[n];
        for (int i = 0; i < n; i++) {
            graph[i] = new LinkedList<>();
        }
        for (int[] edge : flights) {
            graph[edge[0]].add(new int[]{edge[1], edge[2]});
        }
        // 记录起点到任意点的最短路径
        int[] distTo = new int[n];
        Arrays.fill(distTo, Integer.MAX_VALUE);
        distTo[src] = 0;
        // 不用vis数组,为什么?
        Queue<node> queue = new LinkedList<>();
        // 起点入队
        queue.offer(new node(src, 0, 0));
        // 关键在于中转次数 k 的处理
        while (!queue.isEmpty()) {
            // 出队
            node tmp = queue.poll();
            // 说明当前遍历的点都是超过中转次数的(BFS的特性)
            if (tmp.step > k) {
                break;
            }
            // 遍历相邻边
            for (int[] edge : graph[tmp.curId]) {
                int to = edge[0];
                int weight = edge[1];
                // 注意是当前路径的花费cost用于判断更新最短路径,而不是比较之前的最短路径
                if (distTo[to] > tmp.cost + weight) {
                    distTo[to] = tmp.cost + weight;
                    queue.offer(new node(to, distTo[to], tmp.step + 1));
                }
            }
        }
        return distTo[dst] == Integer.MAX_VALUE ? -1 : distTo[dst];
    }
}

三、最多可达成的换楼请求数目(困难)

在这里插入图片描述
在这里插入图片描述
首先想法是暴力,遍历每个request,用一个数组builds记录进出人数情况,进+1,出-1,再最后统计builds=0的情况,问题在于无法统计方案数!!一条条request是一个个方案,但是整个累加求和的过程忽略了request的作用。

考虑到请求数目较小,可以使用状态压缩,用二进制串中的1来表示选择requests中的哪些request,0则表示不选取。这个二进制串的长度就是requests数组的长度。

这类用二进制串表示方案选取的题目,称为状态压缩。

class Solution {
    // 方便全局使用
    int[][] res;
    public int maximumRequests(int n, int[][] requests) {
        res = requests;
        int ans = 0;
        // 请求条数
        int m = requests.length;
        // 遍历全不选 - 全选的方案
        // 最多的情况就是:m个方案全是1:111...
        for (int i = 0; i < (1 << m); i++) {
            int cnt = getCnt(i);
            if (cnt <= ans) continue;
            if (check(i, m)) ans = cnt;
        }
        return ans;
    }
    // check当前方案是否可行
    public boolean check(int x, int m) {
        int[] cnt = new int[20];
        int sum = 0;
        // 有m - 1位
        for (int i = 0; i < m; i++) {
            // 判断具体某一位=1(选取了某一位)
            if (((x >> i) & 1) == 1) {
                int from = res[i][0];
                int to = res[i][1];
                if (++cnt[from] == 1) sum++;
                if (--cnt[to] == 0) sum--;
            }
        }
        return sum == 0;
    }
    // 获取当前方案数的请求数目
    public int getCnt (int x) {
        // 统计当前二进制串中1的个数
        int cnt = 0;
        while (x != 0) {
            cnt++;
            // 每次将x的最后一位置0
            x &= x - 1;
        }
        return cnt;
    }
}

代码中用到的二进制操作一定要熟悉。

下面继续就状态压缩问题做相应练习。

四、括号生成(中等)

在这里插入图片描述
在这里插入图片描述
能使用状态压缩的题目,需要n较小,因为状态压缩实际上是通过遍历二进程串实现暴力穷举。

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> ans = new LinkedList<>();
        for (int i = 0; i < (1 << n * 2); i++) {
            StringBuilder sb = new StringBuilder();
            int count = 0;
            // 遍历当前方案
            for (int j = 0; j < n * 2; j++) {
                if (((i >> j) & 1) == 1) {
                    // 为1的地方,添加左括号
                    sb.append('(');
                    count++;
                } else {
                    // 为0的地方,添加右括号
                    sb.append(')');
                    count--;
                    if (count < 0) {
                        break;
                    }
                }
            }
            if (count == 0) {
                ans.add(sb.toString());
            }
        }
        return ans;
    }
}

如果题目的n数额过大,如何处理?显然,得用搜索DFS、BFS:

class Solution {
    public List<String> ans = new LinkedList<>();
    public List<String> generateParenthesis(int n) {
        dfs(n, 0, 0, "");
        return ans;
    }
    public void dfs(int n, int left, int right, String str) {
        if (left == n && right == n) {
            ans.add(str);
            return;
        }
        // 右括号数 > 左括号数,剪枝
        if (left < right) {
            return;
        }
        if (left < n) {
            dfs(n, left + 1, right, str + '(');
        }
        if (right < n) {
            dfs(n, left, right + 1, str + ')');
        }
    }
}

注意,不要用StringBuilder,因为在函数传递过程中,会共享同一个StringBuilder。

五、完成所有工作的最短时间(困难)

在这里插入图片描述
在这里插入图片描述
能不能用状态压缩,关键看数据量!

通常与状态压缩配合使用的,还有DP,这类算法统称:状态压缩DP,利用状态压缩 + DP的特性,实现问题的求解。

在这里插入图片描述
图源:https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs/solution/zhuang-ya-dp-jing-dian-tao-lu-xin-shou-j-3w7r/

class Solution {
    public int minimumTimeRequired(int[] jobs, int k) {
        int n = jobs.length;
        int[] tot = new int[1 << n];
        // 计算每个子集的工作总时间
        for (int i = 1; i < (1 << n); i++) {
            for (int j = 0; j < n; j++) {
                if ((i & (1 << j)) == 0) continue;
                // left: 代表子集i去除了元素j后剩下的部分总和
                // 可以利用这部分总和计算出新的子集 i 的工作总时间
                int left = (i - (1 << j));
                tot[i] = tot[left] + jobs[j];
                // 只需要找到一个子集即可
                break;
            }
        }
        // dp[i][j]: 前 i 个工人为了完成作业子集 j,需要花费的最大工作时间的最小值
        int[][]dp = new int[k][1 << n];
        // 注意一项工作只能分配给一个工人,一个工人可以分配多个工作
        for (int i = 0; i < (1 << n); i++) {
            // 只有一个工人时,每项工作只能一项项完成
            dp[0][i] = tot[i];
        }
        // 遍历工人数
        for (int i = 1; i < k; i++) {
            // 遍历工作集合,最后答案应该是所有jobs都为1,代表每个工作都被选取了
            for (int j = 0; j < (1 << n); j++) {
                int min = Integer.MAX_VALUE;
                int s = j;
                // 找j的子集s,让第i个工人去做,剩下的i-1个工人去完成子集j-s
                while (s != 0) {
                    // 子集j-s部分
                    int left = j - s;
                    // 保证题目中的最大花费时间,一部分时间是一个工人单独完成子集s所需时间
                    // 另一部分时间是剩余工人,完成j-s剩余子集所需时间
                    int hour = Math.max(dp[i - 1][left], tot[s]);
                    // 找最小值
                    min = Math.min(min, hour);
                    // 找当前j的子集
                    s = (s - 1) & j;
                }
                dp[i][j] = min;
            }
        }
        return dp[k - 1][(1 << n) - 1];
    }
}

状态压缩DP题目,一般来说都是困难级别的,它不仅需要DP,还需要使用与或非等操作,实现对二进串的使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@u@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值