LeetCode第231场周赛

周赛地址:https://leetcode-cn.com/contest/weekly-contest-231/

第一题:检查二进制字符串字段

由于没有前导0,如果字符串里有1,那么字符串第一个一定是1,遍历的时候,先找0再找1,如果有这样的情况,就返回false。
换言之,就是在字符串中找“01”子串。有“01”子串的返回false,否则返回true。
自己写的有点麻烦了,贴一个简洁的代码。

class Solution {
    public boolean checkOnesSegment(String s) {
        return !s.contains("01");
    }
}

第二题:构成特定和需要添加的最少元素

需要注意数据范围。
1 < = n u m s . l e n g t h < = 1 0 5 1 <= nums.length <= 10^{5} 1<=nums.length<=105
1 < = l i m i t < = 1 0 6 1 <= limit <= 10^{6} 1<=limit<=106
− l i m i t < = n u m s [ i ] < = l i m i t -limit <= nums[i] <= limit limit<=nums[i]<=limit
− 1 0 9 < = g o a l < = 1 0 9 -10^{9} <= goal <= 10^{9} 109<=goal<=109
在求和的时候,考虑极端情况, s u m = 1 0 5 × 1 0 6 = 1 0 11 sum=10^{5}\times10^{6}=10^{11} sum=105×106=1011,已经超过int了,所以一些中间结果要用long保存。
这里说向数组中添加的最少数量,每次添加的值,范围要在[-limit, limit]范围内,很容易想到,每次添加就选择limit即可,最后一次根据实际情况选择。

class Solution {
    public int minElements(int[] nums, int limit, int goal) {
        int length = nums.length;
        long sum = 0;
        for (int i : nums) {
            sum += i;
        }
        long diff = Math.abs(goal - sum);
        long mod = diff % limit;
        long div = diff / limit;
        if (mod != 0) {
            div++;
        }
        return (int) div;
    }
}

看题解的时候,看到一个写法,记录下来。

if (diff == 0) {
	return 0;
}
return (diff - 1) / limit + 1;

这里的(diff - 1) / limit + 1相当于Math.ceil(diff / limit)了。

第三题:从第一个节点出发到最后一个节点的受限路径数

根据题目描述和示例,先说下什么叫受限路径。
观察示例1,首先求得1~n各个结点到n结点的最短路径,当 d i s t a n c e [ i ] > d i s t a n c e [ i 的 邻 接 点 ] distance[i]>distance[i的邻接点] distance[i]>distance[i],那么(i, i的邻接点)就是受限路径。
在无向图中,求解1~n各个结点到n结点的最短路径,等于求解从n出发到1~n各个顶点的最短路径,因为无向图中 w ( i , j ) = w ( j , i ) w_{(i, j)}=w_{(j, i)} w(i,j)=w(j,i),所以这里采用单源求解最短路算法Dijkstra即可。
再说求解受限路径的条数,结点1到结点n的受限路径条数=sum(结点1的邻接点到n受限路径条数),避免重复计算,使用记忆化搜索。

class Solution {
    HashMap<Integer, ArrayList<Edge>> graph;// 邻接表存储图
    int[] distance;// Dijkstra用到的distance数组
    boolean[] visit;// Dijkstra用到的visit数组
    PriorityQueue<Edge> priorityQueue;// Dijkstra使用堆优化
    long[] memory;// 用于记忆化搜索的数组
    int MOD = 1000000007;// MOD = 1e9 + 7

    public int countRestrictedPaths(int n, int[][] edges) {
        init(n);
        for (int[] edge : edges) {
            add(edge[0], edge[1], edge[2]);
        }
        dijkstra(n);
        return countLimitPath(1, n);
    }

    /**
     * 给数组初始化分配内存空间
     */
    private void init(int n) {
        graph = new HashMap<>();
        distance = new int[n + 1];
        visit = new boolean[n + 1];
        priorityQueue = new PriorityQueue<>(Comparator.comparingInt(Edge::getW));
        memory = new long[n + 1];
        Arrays.fill(memory, -1);
    }

    /**
     * 向graph添加边建图
     */
    private void add(int u, int v, int w) {
        graph.computeIfAbsent(u, k -> new ArrayList<>()).add(new Edge(u, v, w));
        graph.computeIfAbsent(v, k -> new ArrayList<>()).add(new Edge(v, u, w));
    }

    /**
     * 堆优化的Dijkstra算法
     */
    private void dijkstra(int n) {
        Arrays.fill(distance, 0X3F3F3F3F);
        Arrays.fill(visit, false);
        distance[n] = 0;
        Edge edge;
        int node;
        priorityQueue.offer(new Edge(n, n, distance[n]));
        while (!priorityQueue.isEmpty()) {
            // edge 是 未计算最短路结点集合 到 已计算最短路结点集合 最短的边
            edge = priorityQueue.poll();
            // node 是 未计算最短路结点集合 到 已计算最短路结点集合 最近的结点
            // edge.getU()是已计算最短路的结点,所以下面不会出现edge.getU()
            node = edge.getV();
            if (!visit[node]) {
                // 因为node是从优先队列里取出来的,所以node一定满足最近,标记node为已计算最短路结点
                visit[node] = true;
                // 因为node被标记为已计算最短路结点,更新 已计算最短路结点集合 到 未计算最短路结点集合 的 距离,遍历node的邻接点,看看node结点加入是否造成distance变得更小
                for (Edge e : graph.get(node)) {
                    // e.getV()结点 是 未计算最短路的结点 && n->node + node->v < n->v,更新distance[v]
                    if (!visit[e.getV()] && distance[node] + e.getW() < distance[e.getV()]) {
                        distance[e.getV()] = distance[node] + e.getW();
                        // 把这条边加到优先队列,后序继续找 未计算最短路结点集合 到 已计算最短路结点集合 最短的边
                        priorityQueue.offer(new Edge(node, e.getV(), distance[e.getV()]));
                    }
                }
            }
        }
    }

    /**
     * 计算start->end的受限路径,并把结果记忆化,方便后续搜索
     * 受限路径的定义:结点i到n的最短路>结点i邻接点j到n的最短路,即distance[i]>distance[j](j是i的邻接点)
     * 假设i有两个邻接点j,k,distance[i]>distance[j],distance[i]>distance[k],那么(i,j)和(i,k)都是受限路径
     * 则i到n的受限路径数量:limit(i, n) = limit(j, n) + limit(k, n)
     */
    private int countLimitPath(int start, int end) {
        long result = 0;
        if (memory[start] != -1) {
            return (int) (memory[start] % MOD);
        }
        // n的前一个顶点i到n一定是受限路径,因为distance[i] > distance[n] = 0,前一个顶点不好表示,所以把这个1算到n上
        if (start == end) {
            return 1;
        }
        for (Edge edge : graph.get(start)) {
            if (distance[start] > distance[edge.getV()]) {
                result += countLimitPath(edge.getV(), end);
            }
        }
        // 把结果保存起来
        memory[start] = result % MOD;
        return (int) (result % MOD);
    }
    
	/**
     * 拓扑排序计算start->end的受限路径
     */
    private int countLimitPath(int start, int end) {
        int[] limit = new int[end + 1];
        limit[end] = 1;
        int[][] help = new int[end][2];// 存储结点和结点的distance
        for (int i = 0; i < end; i++) {
            help[i][0] = i + 1;
            help[i][1] = distance[i + 1];
        }
        // 按照distance升序排列(这里实际是一个拓扑排序),可以保证,要求的limit[i]在之前的过程中已经求出过,直接拿过来就能用
        Arrays.sort(help, Comparator.comparingInt(o -> o[1]));
        for (int[] peek : help) {
            int node = peek[0];
            // 访问node的邻接点
            for (Edge edge : graph.get(node)) {
                // distance[node的邻接点] < distance[node],这条路径是受限路径
                if (distance[edge.getV()] < distance[node]) {
                    // node的受限路径条数 = sum(node邻接点的受限路径数)
                    limit[node] = (limit[node] + limit[edge.getV()]) % MOD;
                }
            }
        }
        return limit[start];
    }
}

class Edge {
	// u其实用不到,刷题也不必private,自己给自己找麻烦了233
    private int u, v, w;

    public int getU() {
        return u;
    }

    public void setU(int u) {
        this.u = u;
    }

    public int getV() {
        return v;
    }

    public void setV(int v) {
        this.v = v;
    }

    public int getW() {
        return w;
    }

    public void setW(int w) {
        this.w = w;
    }

    public Edge(int u, int v, int w) {
        this.u = u;
        this.v = v;
        this.w = w;
    }
}

作为扩展,再说下使用数组模拟邻接表的做法,这种做法专业名称叫链式前向星。
参考链接

class Solution {
    static int[] u;// 第i条边的起始结点(其实用不到)
    static int[] v;// 第i条边的结束结点
    static int[] w;// 第i条边的权值
    static int[] next;// 相同起始结点的下一条边的索引
    static int[] head;// 头结点集合
    static int index;// 当前遍历到第几条边

    public static void main(String[] args) {
        int[][] maps = new int[][]{
                {1, 2, 1},
                {2, 3, 2},
                {3, 4, 3},
                {1, 3, 4},
                {4, 1, 5},
                {1, 5, 6},
                {4, 5, 7}
        };
        init(5, 7);
        for (int[] map : maps) {
            add(map[0], map[1], map[2]);
        }
        // 对于1 2 1这条边:edge[0].to = 2; edge[0].next = -1; head[1] = 0;
		// 对于2 3 2这条边:edge[1].to = 3; edge[1].next = -1; head[2] = 1;
		// 对于3 4 3这条边:edge[2].to = 4; edge[2],next = -1; head[3] = 2;
		// 对于1 3 4这条边:edge[3].to = 3; edge[3].next = 0; head[1] = 3;
		// 对于4 1 5这条边:edge[4].to = 1; edge[4].next = -1; head[4] = 4;
		// 对于1 5 6这条边:edge[5].to = 5; edge[5].next = 3; head[1] = 5;
		// 对于4 5 7这条边:edge[6].to = 5; edge[6].next = 4; head[4] = 6;
      	print(5);
    }

    /**
     * n个结点,m条边
     */
    private static void init(int n, int m) {
        u = new int[m];
        v = new int[m];
        w = new int[m];
        next = new int[m];
        head = new int[n + 1];
        Arrays.fill(head, -1);
        index = 0;
    }

    private static void add(int _u, int _v, int _w) {
        u[index] = _u;
        v[index] = _v;
        w[index] = _w;
        next[index] = head[_u];// 以_u为起点上一条边的编号,也就是与这个边起点相同的上一条边的编号
        head[_u] = index++;// 更新以_u为起点上一条边的编号
    }

    private static void print(int n) {
        for (int i = 1; i <= n; i++) {
            System.out.println("起始结点" + i);
            for (int j = head[i]; j != -1; j = next[j]) {
                System.out.println(u[j] + " " + v[j] + " " + w[j]);
            }
        }
    }
}

另外,补上求解最短路的其他算法:Floyd算法、Bellman-Ford算法、SPFA算法。(提一句,这个题后来加数据了,只有优先队列优化的Dijkstra能过,复杂度 O ( M l o g 2 N ) O(Mlog_{2}N) O(Mlog2N)

private void floyd() {
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (graph[i][j] > graph[i][k] + graph[k][j]) {
                    graph[i][j] = graph[i][k] + graph[k][j];
                }
            }
        }
    }
}
/**
 * 返回true表示不存在负环,最短路求解成功
 * 返回false表示存在负环,最短路求解无意义
 */
private static boolean bellmanFord(int n) {
    Arrays.fill(distance, 0X3F3F3F3F);
    distance[n] = 0;
    // 做N-1轮松弛
    for (int i = 1; i < N; i++) {
        // 遍历所有的边,进行松弛
        boolean relaxation = false;// 判断是否有松弛操作,某一轮没有松弛操作,即可退出循环
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            int uv = edge[2];
            int nu = distance[u];
            int nv = distance[v];
            if (nu + uv < nv) {
                distance[v] = nu + uv;
                relaxation = true;
            }
            // 如果是无向图,还要考虑反向
            u = edge[1];
            v = edge[0];
            uv = edge[2];
            nu = distance[u];
            nv = distance[v];
            if (nu + uv < nv) {
                distance[v] = nu + uv;
                relaxation = true;
            }
        }
        if (!relaxation) {
            break;
        }
    }
    // 经过n-1次松弛后,还有nu + uv < nv的情况,说明存在负环
    for (int[] edge : edges) {
        int u = edge[0];
        int v = edge[1];
        int uv = edge[2];
        int nu = distance[u];
        int nv = distance[v];
        if (nu + uv < nv) {
            return false;
        }
    }
    return true;
}
/**
 * 返回false,表示存在负环,最短路无意义
 * 返回true,表示成功求得最短路,最短路在distance数组中
 */
private boolean spfa(int n) {
    Arrays.fill(inQueue, false);
    Arrays.fill(count, 0);
    Arrays.fill(distance, 0X3F3F3F3F);
    queue = new LinkedList<>();
    distance[n] = 0;
    queue.offer(new Edge(n, n, distance[n]));
    inQueue[n] = true;
    count[n]++;
    Edge edge;
    int node;
    while (!queue.isEmpty()) {
        edge = queue.poll();// edge 是 未计算最短路结点集合 到 已计算最短路结点集合 的 任意一条边
        node = edge.getV();// node 是 edge的一个结点
        inQueue[node] = false;// 标记node出队列
        for (Edge e : graph.get(node)) {// 遍历node的邻接点,判断是否需要松弛
            // n->node + node->v < n->v,进行松弛
            if (distance[node] + e.getW() < distance[e.getV()]) {
                distance[e.getV()] = distance[node] + e.getW();
                // spfa允许结点多次进入队列
                if (!inQueue[e.getV()]) {
                    queue.offer(new Edge(node, e.getV(), distance[e.getV()]));
                    // 标记入队
                    inQueue[e.getV()] = true;
                    // 统计入队次数
                    count[e.getV()]++;
                    // 入队次数大于结点总数,说明存在负环
                    if (count[e.getV()] > N) {
                        return false;
                    }
                }
            }
        }
    }
    return true;
}

第四题:使所有区间的异或结果为零

把题解写到代码里了,实在是太难了,看着别人的题解想了好久,大概能看明白了,自己写,哎,还是写不出来。

class Solution {
    /**
     * 分析前两个长度为k的区间
     * 区间a:nums[0] ~ nums[k - 1]这k个数字,异或结果是0
     * 区间b:nums[1] ~ nums[k]这k个数字,异或结果是0
     * 区间a和区间b公共部分是nums[1] ~ nums[k - 1],区间a的异或 = 0 = 区间b的异或,可以得到结论:nums[0] = nums[k]
     * 假设,将nums放到一个二维数组中,二维数组的列数col = k,二维数组的行数row = (length + k - 1) / k
     * 同理,可以得到nums的最后状态如下
     * 第0列:nums[0 * k] = nums[1 * k] = nums[2 * k] = …… = nums[(row - 1) * k]
     * 第1列:nums[0 * k + 1] = nums[1 * k + 1] = nums[2 * k + 1] = …… = nums[(row - 1) * k + 1]
     * ……
     * 第k - 1列:nums[0 * k + k -1] = nums[1 * k + k -1] = nums[2 * k + k -1] = …… = nums[(row - 1) * k + k -1]
     * 当然,最后一行可能不足k个,不过不影响
     * 把每一列看做一组,最后的结果是第1组所有数字都变成a,第2组所有数字都变成b,……,第k - 1组所有数字都变成x
     * 使得a ^ b ^ …… ^ x = 0
     * 因为同一组里最终都变成同一个数,我们只需要考虑[0, k - 1]这个区间即可
     * nums[i]变成什么,就是我们要分析的,假设nums[i]最终变成了goal
     * 这个goal就有两种情况,它们对应的代价是不同的
     * 用cost表示第i列数字都改成goal需要的代价,用frequency[i].get(goal)表示原来第i列中,goal数字出现的次数,用size[i]表示第i列数字总数量
     * cost = size[i] - frequency[i].get(goal)
     * 1.goal不来自第i列的某个值:frequency[i].get(goal) = 0, cost = size[i] - frequency[i].get(goal) = size[i]
     * 2.goal来自第i列的某个值:cost = size[i] - frequency[i].get(goal)
     * 介绍dp数组,dp[i - 1][j]的结果代表:nums[0] ^ nums[1] ^ …… ^ nums[i - 1] = j所需要的最小代价
     * 我们要把nums[i]变成goal,那么nums[0] ^ nums[1] ^ …… ^ nums[i - 1] ^ nums[i] = j ^ goal,即dp[i][j ^ goal]
     * 从而,状态转移方程:dp[i][j ^ goal] = dp[i - 1][j] + cost[goal]
     * 这里一定要分清楚,第i列的目标值goal,前i列的异或值j ^ goal,这两者的区别,一开始理解的时候混成一个意思了
     * 然后分析这个goal的情况,goal的情况不同,cost[goal]就不同
     * 最后dp[k][0]就是答案
     * 观察数据范围,nums[i]∈[0,1023],任何两个nums[i]异或的结果也不会超过这个范围
     */
    public int minChanges(int[] nums, int k) {
        int length = nums.length, MAX = 1024, INF = 0X3F3F3F3F, cost;
        int[] size = new int[k];
        int[][] dp = new int[k + 1][MAX];
        // HashMap数组,frequency[i]表示第i组的情况,key是数字,value是这个数字的频次
        HashMap<Integer, Integer>[] frequency = new HashMap[k];
        // 统计第i组的数字总数量,统计第i列中,number出现的频次
        for (int i = 0; i < length; i++) {
            int index = i % k;
            int number = nums[i];
            size[index]++;
            if (frequency[index] == null) {
                frequency[index] = new HashMap<>();
            }
            // 先get(number),如果是null,put(number, 1),否则让value + 1
            frequency[index].merge(number, 1, Integer::sum);
        }
        // dp数组初始化
        for (int i = 0; i <= k; i++) {
            for (int j = 0; j < MAX; j++) {
                dp[i][j] = INF;
            }
        }
        dp[0][0] = 0;
        for (int i = 1; i <= k; i++) {
            // 第i列变成的goal不来自原来的第i - 1列,cost = size[i - 1],意味着cost只和i - 1列数字个数有关
            cost = size[i - 1];
            // 要dp[i][j]想最小,就要取dp[i - 1][j]的最小值 + cost
            int min = INF;
            // 找dp[i - 1][j]的最小值(其实这里包含goal来自第i - 1列数字的情况,但是这里cost较大,还会被下面的更小的cost情况更新掉)
            for (int j = 0; j < MAX; j++) {
                min = Math.min(min, dp[i - 1][j]);
            }
            // 更新dp数组
            for (int j = 0; j < MAX; j++) {
                dp[i][j] = min + cost;
            }
            // 第i - 1列变成的goal来自原来的第i - 1列,cost = size[i- 1] - frequency[i - 1].get(goal)
            // 遍历第i - 1列所有的数字
            for (int number : frequency[i - 1].keySet()) {
                for (int j = 0; j < MAX; j++) {
                    cost = size[i - 1] - frequency[i - 1].get(number);
                    dp[i][j ^ number] = Math.min(dp[i][j ^ number], dp[i - 1][j] + cost);
                }
            }
        }
        return dp[k][0];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
自动控制节水灌溉技术的高低代表着农业现代化的发展状况,灌溉系统自动化水平较低是制约我国高效农业发展的主要原因。本文就此问题研究了单片机控制的滴灌节水灌溉系统,该系统可对不同土壤的湿度进行监控,并按照作物对土壤湿度的要求进行适时、适量灌水,其核心是单片机和PC机构成的控制部分,主要对土壤湿度与灌水量之间的关系、灌溉控制技术及设备系统的硬件、软件编程各个部分进行了深入的研究。 单片机控制部分采用上下位机的形式。下位机硬件部分选用AT89C51单片机为核心,主要由土壤湿度传感器,信号处理电路,显示电路,输出控制电路,故障报警电路等组成,软件选用汇编语言编程。上位机选用586型以上PC机,通过MAX232芯片实现同下位机的电平转换功能,上下位机之间通过串行通信方式进行数据的双向传输,软件选用VB高级编程语言以建立友好的人机界面。系统主要具有以下功能:可在PC机提供的人机对话界面上设置作物要求的土壤湿度相关参数;单片机可将土壤湿度传感器检测到的土壤湿度模拟量转换成数字量,显示于LED显示器上,同时单片机可采用串行通信方式将此湿度值传输到PC机上;PC机通过其内设程序计算出所需的灌水量和灌水时间,且显示于界面上,并将有关的灌水信息反馈给单片机,若需灌水,则单片机系统启动鸣音报警,发出灌水信号,并经放大驱动设备,开启电磁阀进行倒计时定时灌水,若不需灌水,即PC机上显示的灌水量和灌水时间均为0,系统不进行灌水。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值