第十三届蓝桥杯

目录

九进制转十进制

顺子日期(模拟)

刷题统计(模拟)

修剪灌木(模拟 + 枚举)

X 进制减法(贪心)

统计子矩阵(前缀和 + 双指针)

积木画(状态压缩dp)

扫雷(图论 + 哈希表 + 有向图的遍历)

李白打酒加强版(线性dp)

砍竹子(思维题)


我本来是python组,为了写一下题目,所以用python和go语言编写代码,对于c++ B组来说后面五题相对难一点,考察了二维前缀和,贪心,双指针,状态压缩dp,抽象为图论问题,图的遍历,线性dp等知识点。

九进制转十进制

九进制正整数 (2022)9 转换成十进制等于多少?直接使用x进制转十进制的公式计算即可:

package main

import (
	"fmt"
)

func power(x int, n int) int {
	res := 1
	for i := 0; i < n; i++ {
		res *= x
	}
	return res
}

func main() {
	fmt.Println(2*power(9, 3) + 2*power(9, 1) + 2)
}
if __name__ == '__main__':
    print(2 * 9 ** 3 + 2 * 9 + 2)

顺子日期(模拟)

小明特别喜欢顺子。顺子指的就是连续的三个数字: 123、 456 等。顺子日期指的就是在日期的 yyyymmdd 表示法中,存在任意连续的三位数是一个顺子的日期。例如 20220123 就是一个顺子日期,因为它出现了一个顺子: 123;而 20221023 则不是一个顺子日期,它一个顺子也没有。小明想知道在整个 2022年份中,一共有多少个顺子日期。这里是将012也看成是顺子日期,我们可以枚举2022年的每一天,判断当前日期的yyyymmdd表示中是否是顺子日期即可:

package main

import (
	"fmt"
)

// 判断当前的s是否是顺子日期
func check(s string) bool {
	n := len(s)
	for i := 0; i+2 < n; i++ {
		// 字符串中的每一个字符是uint8类型, 也即是一个数字
		if s[i+1] == s[i]+1 && s[i+2] == s[i+1]+1 {
			return true
		}
	}
	return false
}

func main() {
	months := []int{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
	year, month, day := 2022, 1, 1
	res := 0
	// 枚举2022年的每一天
	for i := 0; i < 365; i++ {
		// 格式化字符串
		s := fmt.Sprintf("%04d%02d%02d", year, month, day)
		if check(s) {
			res += 1
		}
		day += 1
		if day > months[month] {
			day = 1
			month += 1
		}
	}
	fmt.Println(res)
}
class Solution:
    def check(self, s: str):
        i = 0
        while i + 2 < len(s):
            if ord(s[i + 1]) == ord(s[i]) + 1 and ord(s[i + 1]) + 1 == ord(s[i + 2]):
                return True
            i += 1
        return False

    def process(self):
        months = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
        year, month, day = 2022, 1, 1
        res = 0
        for i in range(365):
            # 使用formt函数函数输出, 左补0
            s = "{:04d}{:02d}{:02d}".format(year, month, day)
            if self.check(s):
                res += 1
                # print(s)
            day += 1
            # 进入下一个月
            if day > months[month]:
                day = 1
                month += 1
        return res


if __name__ == '__main__':
    print(Solution().process())

刷题统计(模拟)

小明决定从下周一开始努力刷题准备蓝桥杯竞赛。他计划周一至周五每天做 a 道题目,周六和周日每天做 b 道题目。请你帮小明计算,按照计划他将在第几天实现做题数大于等于 n 题?

思路分析:我们以7天为一周,计算n道题目至少需要几周来刷,剩余的题目肯定不足一周然后计算剩余的题目需要几天来刷,两部分加起来的天数就是答案:

package main

import "fmt"

func main() {
	var (
		a int64
		b int64
		n int64
	)
	fmt.Scan(&a, &b, &n)
	res := int64(0)
	week := n / (5*a + 2*b)
	res += 7 * week
	last := n % (5*a + 2*b)
	i := 0
	for last > 0 {
		if i < 5 {
			last -= a
		} else {
			last -= b
		}
		res += 1
		i += 1
	}
	fmt.Println(res)
}
class Solution:
    def process(self):
        a, b, n = map(int, input().split())
        week = n // (5 * a + 2 * b)
        total = 7 * week
        last = n % (5 * a + 2 * b)
        i = 0
        # 剩余的题目不足一周模拟一下
        while last > 0:
            if i < 5:
                last -= a
            else:
                last -= b
            total += 1
            i += 1
        print(total)


if __name__ == '__main__':
    Solution().process()

修剪灌木(模拟 + 枚举)

爱丽丝要完成一项修剪灌木的工作。有 N 棵灌木整齐的从左到右排成一排。爱丽丝在每天傍晚会修剪一棵灌木,让灌木的高度变为 0 厘米。爱丽丝修剪灌木的顺序是从最左侧的灌木开始,每天向右修剪一棵灌木。当修剪了最右侧的灌木后,她会调转方向,下一天开始向左修剪灌木。直到修剪了最左的灌木后再次调转方向。然后如此循环往复。灌木每天从早上到傍晚会长高 1 厘米,而其余时间不会长高。在第一天的早晨,所有灌木的高度都是 0 厘米。爱丽丝想知道每棵灌木最高长到多高。

思路分析:分析题目可以知道相当于是已知一个数轴,数轴上的整数点表示的要修剪的灌木,对于第i颗灌木,高度取到最大的有两种可能,第一种是从左往右-->从右往左裁,第二种是从右往左--->从左往右,并且需要注意裁剪的时间:是先长再裁剪,对于第一种高度为2 * (n - i),第二种高度为2 * (i - 1),枚举所有的位置,对于每一个位置两种情况取一个max即可:

package main

import "fmt"

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func main() {
	var n int
	fmt.Scan(&n)
	for i := 1; i <= n; i++ {
		fmt.Println(max(2*(n-i), 2*(i-1)))
	}
}
if __name__ == '__main__':
    n = int(input())
    for i in range(1, n + 1):
        print(max(2 * (i - 1), 2 * (n - i)))

X 进制减法(贪心)

进制规定了数字在数位上逢几进一。X 进制是一种很神奇的进制,因为其每一数位的进制并不固定!例如说某种 X 进制数,最低数位为二进制,第二数位为十进制,第三数位为八进制,则X 进制数 321 转换为十进制数为 65。现在有两个 X 进制表示的整数 A 和 B,但是其具体每一数位的进制还不确定,只知道 A 和 B 是同一进制规则,且每一数位最高为 N 进制,最低为二进制。请你算出 A − B 的结果最小可能是多少。请注意,你需要保证 A 和 B 在 X 进制下都是合法的,即每一数位上的数字要小于其进制。

思路分析:这道题目的第一个难点是在于理解题目的意思上,对于一般的进制来说每一位的进制进位都是一样的,而对于x进制来说每一位的进制可能不一样,我们可以以题目中给出的例子理解65是怎么样计算出来的,其实也是类似于10进制的进位,10进制是逢十进一,而对于当前的x进制来说我们需要看每一位进1需要多少次,以x进制的321为例:最低位1需要1次,第二位那么需要2 * 2 = 4次,对于第三位那么需要3 * 10 * 2,那么总共需要1 + 4 + 60 = 65次才可以表示x进制,这样我们就知道了当前的x进制如何转换为10进制,题目中规定了A >= B,所以我们不妨设A,B最大位数为N位,如果不足N位那么最高位补0即可,我们可以使用表达式表达出A - B,而第二个难点在于A - B公式的工具表达提取出对应的pi,找到使得A - B最小每一个pi需要满足什么条件,最终化简之后可以发现每一个pi都是独立的要想使得A - B最小那么使得每一个pi最小即可,那么pi = max{a[i] + 1,b[i] + 1,2},因为进制肯定是比当前的数字至少大1的所以需要加上1,而且最小的进制为2进制,最终使用秦九韶算法计算出A - B的值即可:

package main

import "fmt"

func max1(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// 求解三个int整数的最大值
func max2(a, b, c int) int {
	max := a
	if a < b {
		max = b
	}
	if max < c {
		max = c
	}
	return max
}

func main() {
	const N = 100010
	var (
		n, m1, m2 int
		// a, b存储每一位上的数字
		a, b [N]int
	)
	fmt.Scan(&n, &m1)
    // 逆序存储, 也即低位对齐
	for i := m1 - 1; i >= 0; i-- {
		fmt.Scan(&a[i])
	}
	fmt.Scan(&m2)
	for i := m2 - 1; i >= 0; i-- {
		fmt.Scan(&b[i])
	}
	res, mod := 0, 1000000007
	m := max1(m1, m2)
	for i := m - 1; i >= 0; i-- {
        // 秦九韶算法计算A - B, 每一个pi都是独立的所以要想使得A - B最小那么需要使得每一个pi最小
        // 那么每一个pi填的数字就是确定的
		res = (res*(max2(a[i]+1, b[i]+1, 2)) + a[i] - b[i]) % mod
	}
	fmt.Println(res)
}
if __name__ == '__main__':
    n = int(input())
    m1 = int(input())
    a = list(map(int, input().split()))
    # 这里是低位对齐这样会比较好操作, 所以需要翻转一下
    a = a[::-1]
    m2 = int(input())
    b = list(map(int, input().split()))
    b = b[::-1]
    m = max(m1, m2)
    # 高位补0
    for i in range(m1, m):
        a.append(0)
    for i in range(m2, m):
        b.append(0)
    res = 0
    mod = 10 ** 9 + 7
    for i in range(m - 1, -1, -1):
        res = (res * max(2, a[i] + 1, b[i] + 1) + a[i] - b[i]) % mod
    print(res)

统计子矩阵(前缀和 + 双指针)

给定一个 N × M 的矩阵 A,请你统计有多少个子矩阵 (最小 1 × 1,最大N × M) 满足子矩阵中所有数的和不超过给定的整数 K?

思路分析:这道题目最容易想到的是维护一个二维前缀和s,然后枚举矩阵的左上角与右下角两个点的坐标,使用维护的二维前缀和计算出每一个子矩阵的和,统计所有符合条件的子矩阵的个数即可,时间复杂度为O(n ^ 4),由题目可知,N,M最大为500,所以需要将时间复杂度控制在O(n ^ 3)以内,O(n ^ 4)肯定会超时,所以至少需要优化掉一维的枚举才可以通过,我们可以这样想,两个维度用来维护矩阵的两行,通过当前矩阵两行限制的区域限制两列的位置进而确定符合要求的子矩阵,因为矩阵中所有元素大于等于0,所以矩阵右边的列越往右走那么和是越大的,所以存在单调性,那么我们考虑二分或者双指针来解决,这里使用双指针来解决会比较方便,能否使用双指针解决的充分必要条件是一个指针随着另外一个指针单调往左走或者往右走,可以证明当指针r往右走的时候,指针l也是单调往右走的(r往右走的时候为r',l如果往左走为l'那么r最左边的位置为l'这样就矛盾了所以当r往右走的时候l肯定是往左走的),这样我们就可以找到当前以r为右边界中左边最靠左的列的位置,l与r之间的个数就是当前由两行限制的以r为右边界中符合要求子矩阵的个数:

一维前缀和(s[i]维护第0行到第i行的每一列的前缀和,s[i][j]表示第i行第j列中第0个位置到第j个位置的一维前缀和):

package main

import "fmt"

func main() {
	const N = 510
	var (
		n, m, k int
		// s存储每一列的前缀和
		s [N][N]int
	)
	fmt.Scan(&n, &m, &k)
	for i := 1; i <= n; i++ {
		for j := 1; j <= m; j++ {
			fmt.Scan(&s[i][j])
			s[i][j] += s[i-1][j]
		}
	}
	res := 0
	for i := 1; i <= n; i++ {
		for j := i; j <= n; j++ {
			tot := 0
			l, r := 1, 1
			for r <= m {
				tot += s[j][r] - s[i-1][r]
				for tot > k {
					tot -= s[j][l] - s[i-1][l]
					l += 1
				}
				res += r - l + 1
				r += 1
			}
		}
	}
	fmt.Println(res)
}

python:(超时):

class Solution:
    def process(self):
        n, m, k = map(int, input().split())
        # s存储每一列的前缀和
        s = [[0] * (m + 10) for i in range(n + 10)]
        a = list()
        for i in range(n):
            a.append(list(map(int, input().split())))
        for i in range(1, n + 1):
            for j in range(1, m + 1):
                s[i][j] += s[i - 1][j] + a[i - 1][j - 1]
        res = 0
        # 枚举上下两行
        for i in range(1, n + 1):
            for j in range(i, n + 1):
                # 枚举左右两列的的位置
                l = r = 1
                tot = 0
                while r <= m:
                    tot += s[j][r] - s[i - 1][r]
                    while tot > k:
                        tot -= s[j][l] - s[i - 1][l]
                        l += 1
                    # l, r之间的矩阵都是满足条件那么[l, r]之间的个数就是当前固定两行的位置以及对应的每一个r与之最左边的位置l之间满足子矩阵的个数
                    res += r - l + 1
                    r += 1
        print(res)


if __name__ == '__main__':
    Solution().process()

二维前缀和(O(n ^ 4):超时):

package main

import "fmt"

func main() {
	const N = 510
	var (
		n, m, k int
		// s为二维前缀和
		s [N][N]int
	)
	fmt.Scan(&n, &m, &k)
	for i := 1; i <= n; i++ {
		for j := 1; j <= m; j++ {
			fmt.Scan(&s[i][j])
			s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1]
		}
	}
	res := 0
	// 枚举子矩阵的左上角和右下角的位置, 左上角的位置为(i, j), 右下角的位置为(x, y)
	for i := 1; i <= n; i++ {
		for j := 1; j <= m; j++ {
			for x := i; x <= n; x++ {
				for y := j; y <= m; y++ {
					// 使用s计算二维前缀和判断当前的子矩阵是否满足条件(画图确定当前子矩阵前缀和的计算公式)
					t := s[x][y] - s[i-1][y] - s[x][j-1] + s[i-1][j-1]
					if t <= k {
						res += 1
					}
				}
			}
		}
	}
	fmt.Println(res)
}

积木画(状态压缩dp)

小明最近迷上了积木画,有这么两种类型的积木,分别为 I 型(大小为 2个单位面积)和 L 型(大小为 3 个单位面积):

同时,小明有一块面积大小为 2 × N 的画布,画布由 2 × N 个 1 × 1 区域构成。小明需要用以上两种积木将画布拼满,他想知道总共有多少种不同的方式?积木可以任意旋转,且画布的方向固定。

思路分析: 一看这种题目直觉上是状态压缩dp,这道题目类似于之前蒙德里安的梦想那道题,当前的题目是其简化版(当前只有两行),可以使用类似的思想我们可以一列一列地填,可以定义一个二维数组或者列表f(第二维长度为4表示4种状态),其中f(i,j)表示操作完第i - 1列且第i列的状态的j的方案数目,操作完第i列的意思是前i - 1列都已经被填满了(合法),我们在状态计算的时候需要考虑当前的状态是否可以由前一个状态转移过来,因为只有两行,所以对于前一个状态来说有4种情况(第i - 1列),对于当前的状态来说有4种情况(第i列),所以上一个状态与当前状态的转移情况有4 * 4 = 16种情况,为了方便状态转移,我们可以定义一个二维数组或者列表g(4 * 4),因为上一个状态转移到当前状态的转移的数量比较小,所以可以手算一下当前状态是否可以转移到其他的状态,其中g的行表示上一列的状态,列表示当前的状态,对于上一列的状态可以分为4种,分别为00,01,10,11,对于上一列每一种状态我们将其填满,看可以转移到哪些状态,可以状态到的状态对应位置标记为1,表示可以由上一个状态转移到当前的状态并且方案数目为1(反正我们必须要将上一列填满才能够合法转移到当前的状态,上一列的状态填的情况影响到的就是当前的状态那么得到的就是当前这一列的状态),我们可以使用三层循环来枚举,第一层循环枚举第i列,第二层循环枚举上一列的状态,第三层循环枚举当前这一列的状态,计算上一个状态是否可以转移到当前的状态,最终f(n + 1,0)就是答案,表示操作完第n列且第n + 1的状态为0:

package main

import "fmt"

func main() {
	// g存储上一个状态是否可以转移到当前的状态, 为1表示可以转移并且方案数目为1
	g := [][]int{
		{1, 1, 1, 1},
		{0, 0, 1, 1},
		{0, 1, 0, 1},
		{1, 0, 0, 0}}
	var n int
	fmt.Scan(&n)
	const (
		N   = 10000010
		mod = 1000000007
	)
	var f [N][4]int
    // f[1][0]为边界情况, 表示前0列已经填满并且第1列的状态为0的方案数目为1
	f[1][0] = 1
    // i枚举第i列
	for i := 1; i <= n; i++ {
        // j表示上一个状态
		for j := 0; j < 4; j++ {
            // k表示当前第i + 1的状态, 乘法表示是否可以由上一个状态转移过来
			for k := 0; k < 4; k++ {
                // f[i][j]表示上一列的状态为j, g[j][k]表示上一个状态是否可以转移到当前的状态
				f[i+1][k] = (f[i+1][k] + f[i][j]*g[j][k]) % mod
			}
		}
	}
	fmt.Println(f[n+1][0])
}

python(超时):

class Solution:
    def process(self):
        n = int(input())
        f = [[0] * 4 for i in range(n + 5)]
        g = [[1, 1, 1, 1],
             [0, 0, 1, 1],
             [0, 1, 0, 1],
             [1, 0, 0, 0]]
        f[1][0] = 1
        mod = 10 ** 9 + 7
        for i in range(1, n + 1):
            for j in range(4):
                for k in range(4):
                    f[i + 1][k] = (f[i + 1][k] + f[i][j] * g[j][k]) % mod
        print(f[n + 1][0])


if __name__ == '__main__':
    Solution().process()

扫雷(图论 + 哈希表 + 有向图的遍历)

小明最近迷上了一款名为《扫雷》的游戏。其中有一个关卡的任务如下,在一个二维平面上放置着 n 个炸雷,第 i 个炸雷 (xi,yi, ri) 表示在坐标 (xi,yi) 处存在一个炸雷,它的爆炸范围是以半径为 ri 的一个圆。为了顺利通过这片土地,需要玩家进行排雷。玩家可以发射 m 个排雷火箭,小明已经规划好了每个排雷火箭的发射方向,第 j 个排雷火箭 (xj, yj, rj) 表示这个排雷火箭将会在 (xj,yj) 处爆炸,它的爆炸范围是以半径为 rj 的一个圆,在其爆炸范围内的炸雷会被引爆。同时,当炸雷被引爆时,在其爆炸范围内的炸雷也会被引爆。现在小明想知道他这次共引爆了几颗炸雷?你可以把炸雷和排雷火箭都视为平面上的一个点。一个点处可以存在多个炸雷和排雷火箭。当炸雷位于爆炸范围的边界上时也会被引爆。

思路分析:分析题目可以知道排雷火箭引爆炸雷的时候会产生连锁反应,一个点与另外一个点有联系,其中需要使用到一个比较重要的思想:抽象为图论问题的思想,我们可以将排雷火箭和炸雷看成是一个点,如果一个点可以引爆另外一个点那么我们就可以将当前的点向另外一个点连一条有向边,注意这里是有向边,因为在引爆的时候并不是相互引爆的,这是由两个点的引爆半径决定的,一个点可以引爆另外一个点但是反过来不一定成立,所以是有向边,这样我们就可以将问题抽象为一个图论的问题,等价于我们已知若干个起点,问从当前的起点出发最多可以遍历多少个点,分析到这里可以发现实际上是有向图的遍历问题,可以使用dfs或者bfs遍历都可以,使用dfs遍历会比较好写一点(不用写队列),注意这里不能够使用并查集来做,因为并查集解决的是无向图的连通性问题,集合中的点是相互的也就是集合中的任意两个点都是相互的;因为这里的点比较多的,这里使用一个比较取巧的方法我们可以只存在炸雷对应的点的信息,包括x,y,r,可以将点的信息存储到一个结构体数组中,然后后面在输入排雷火箭的时候遍历图即可,由于后面需要遍历炸雷,所以需要将所有炸雷对应的点的信息存储到哈希表中,其中一个比较好的处理方法是将点的横坐标和纵坐标转化为字符串并且以空格将其连接起来,然后将其存储到内置的哈希表中,但是这道题目会卡常,如果使用内置的哈希表只过了一半的数据,这里可以使用数组来模拟哈希表,也即自己手写一个哈希表,但是使用go语言提交上去最后两个数据还是超时了,c++语言就不会超时,手写哈希表的时候我们需要将二维坐标转化为一个int64的整数,这样每一个二维坐标对应一个int64的整数(x,y最大是10 ^ 9所以可以看成是10 ^ 9 + 1进制),因为多个炸雷可能在同一个位置,我们可以使用一个id数组来存储二维坐标下对应的引爆半径最大的那个点的信息,数组的值为输入的索引值 + 1,这样我们在引爆的时候从爆炸范围最大的点进行引爆,这恰恰包含了爆炸范围较小的点,这里可以使用dfs遍历,每输入一个排雷火箭那么我们枚举半径为r的爆炸范围判断是否存在炸雷,因为枚举半径不好枚举,这里可以枚举一个上下左右都是r的矩形,然后判断对应的点是否在圆内或者圆上即可,如果满足并且在哈希表中可以找到对应的坐标说明炸雷存在那么从这个炸雷出发,在遍历的时候需要使用一个st数组来标记哪些点已经访问过了,最终我们遍历所有标记过的炸雷对其计数即可:

手写哈希表(超时):

package main

import "fmt"

type Circle struct {
	x, y, r int
}

func sqr(x int) int {
	return x * x
}

// 映射为对应的值
func find(x, y int) int64 {
	key := getKey(x, y)
	t := (key%M + M) % M
	for mp[t] != -1 && int64(mp[t]) != key {
		t += 1
		if t == M {
			t = 0
		}
	}
	return t
}

// 看成是1000000001进制(x, y的坐标最大是10 ^ 9), find函数将x, y映射成int64的整数
func getKey(x, y int) int64 {
	return int64(x*1000000001 + y)
}

func dfs(x, y, r int) {
    // 标记已经被访问
	key := find(x, y)
	st[key] = 1
	for i := x - r; i <= x+r; i++ {
		for j := y - r; j <= y+r; j++ {
			if sqr(i-x)+sqr(j-y) <= sqr(r) {
				k := find(i, j)
				if id[k] != 0 && st[k] == 0 {
					dfs(i, j, cir[id[k]].r)
				}
			}
		}
	}
}

const (
	N = 50010
    // 找一个比较大的质数
	M = 999993
)

var (
	mp, id, st = [M]int64{}, [M]int64{}, [M]int64{}
	cir        [N]Circle
)

func main() {
	// 将哈希表的值置为-1, 表示对应的位置还没有值
	for i := 0; i < M; i++ {
		mp[i] = -1
	}
	var (
		n, m int
	)
	fmt.Scan(&n, &m)
	for i := 1; i <= n; i++ {
		var (
			x, y, r int
		)
		fmt.Scan(&x, &y, &r)
		cir[i].x, cir[i].y, cir[i].r = x, y, r
		key := find(x, y)
		if mp[key] == -1 {
			mp[key] = getKey(x, y)
		}
        // 之前没有标记或者之前已经标记了但是当前的引爆半径更大所以需要更新
		if id[key] == 0 || cir[id[key]].r < r {
			id[key] = int64(i)
		}
	}
    // 在输入每一个排雷火箭的时候找到引爆半径内的炸雷使用dfs标记所有可以引爆的炸雷
	for k := 1; k <= m; k++ {
		var (
			x, y, r int
		)
		fmt.Scan(&x, &y, &r)
		for i := x - r; i <= x+r; i++ {
			for j := y - r; j <= y+r; j++ {
                // 判断是否在圆内
				if sqr(i-x)+sqr(j-y) <= sqr(r) {
					key := find(i, j)
					if id[key] != 0 && st[key] == 0 {
						dfs(i, j, cir[id[key]].r)
					}
				}
			}
		}
	}

	// 枚举所有标记为被引爆的位置
	res := 0
	for i := 1; i <= n; i++ {
		key := find(cir[i].x, cir[i].y)
		if st[key] == 1 {
			res += 1
		}
	}
	fmt.Println(res)
}

内置的哈希表(超时):

package main

import (
	"fmt"
	"strconv"
)

// Circle 存储炸雷的坐标以及爆炸半径
type Circle struct {
	x, y, r int
}

func sqr(x int) int {
	return x * x
}

// 从当前的x, y位置引爆其他的炸雷
func dfs(x, y, r int) {
	key := strconv.Itoa(x) + " " + strconv.Itoa(y)
	st[key] = 1
	for i := x - r; i <= x+r; i++ {
		for j := y - r; j <= y+r; j++ {
			if sqr(i-x)+sqr(j-y) <= sqr(r) {
				k := strconv.Itoa(i) + " " + strconv.Itoa(j)
				idIndex, idIsExist := id[k]
				_, visIsExist := st[k]
				if idIsExist && !visIsExist {
					dfs(i, j, cir[idIndex].r)
				}
			}
		}
	}
}

const N = 50010

var (
	// 声明为全局变量会好一点
	mp, id, st = make(map[string]int), make(map[string]int), make(map[string]int)
	// 存储炸雷的信息
	cir [N]Circle
)

func main() {
	var (
		n, m int
	)
	fmt.Scan(&n, &m)
	// 存储所有的炸雷
	for i := 1; i <= n; i++ {
		var (
			x, y, r int
		)
		fmt.Scan(&x, &y, &r)
		cir[i].x, cir[i].y, cir[i].r = x, y, r
		key := strconv.Itoa(x) + " " + strconv.Itoa(y)
		// 将当前的二维坐标转化为map的键
		mp[key] = 1
		index, isExist := id[key]
		if !isExist || cir[index].r < r {
			// id存储当前key相同最大的爆炸半径
			id[key] = i
		}
	}
	for k := 1; k <= m; k++ {
		var (
			x, y, r int
		)
		fmt.Scan(&x, &y, &r)
		for i := x - r; i <= x+r; i++ {
			for j := y - r; j <= y+r; j++ {
				if sqr(i-x)+sqr(j-y) <= sqr(r) {
					key := strconv.Itoa(i) + " " + strconv.Itoa(j)
					idIndex, idIsExist := id[key]
					_, visIsExist := st[key]
					if idIsExist && !visIsExist {
						dfs(i, j, cir[idIndex].r)
					}
				}
			}
		}
	}
	// 枚举所有被引爆的位置
	res := 0
	for i := 1; i <= n; i++ {
		key := strconv.Itoa(cir[i].x) + " " + strconv.Itoa(cir[i].y)
		_, isExist := st[key]
		if isExist {
			res += 1
		}
	}
	fmt.Println(res)
}

李白打酒加强版(线性dp)

话说大诗人李白,一生好饮。幸好他从不开车。一天,他提着酒壶,从家里出来,酒壶中有酒 2 斗。他边走边唱:无事街上走,提壶去打酒。逢店加一倍,遇花喝一斗。这一路上,他一共遇到店 N 次,遇到花 M 次。已知最后一次遇到的是花,他正好把酒喝光了。请你计算李白这一路遇到店和花的顺序,有多少种不同的可能?注意:壶里没酒 ( 0 斗) 时遇店是合法的,加倍后还是没酒;但是没酒时遇花是不合法的。

思路分析:分析题目可以知道最容易想到的是暴力枚举,对于当前的N + M个元素我们尝试枚举所有可能的排列,最终判断最后一次是否是花并且把酒喝光了,由于N,M最大是100所以肯定会超时,我们需要将时间复杂度控制在O(n ^ 3)以内,因为暴力枚举的做法其实是枚举所有可能的方案,所以我们可以换一种类似的方法,可以使用dp来解决,dp其实可以看成是暴力枚举的优化,每一个状态其实表示的是一个集合所以会比较快一点,分析题目可以发现这道题目其实是线性dp的题目(类似于数字三角形,从两个方向进行转移),dp主要从两个方面考虑:① 状态表示;② 状态计算;状态表示一般是根据题目中有多少个需要使用到的动态变化的变量进行定义,可以发现店的数量,花的数量和当前酒的数量都是需要使用到的动态变化的量,所以这里可以定义一个三维数组f,其中f[i][j][k]表示一共遇到i个店,j朵花,当前有k斗酒的方案数目;如果进行状态计算呢?状态计算对应集合的划分,一般是找最后一个不同点,我们可以依据最后一次遇到的花还是店将当前的f[j][j][k]分成两个集合,并且可以发现当前划分的这两个集合是不重不漏的,在计算方案数目的时候需要满足不重不漏这两个条件,如果求解最值则要求不漏,但是可以重复,我们在状态计算的时候可以使用三层循环来枚举,第一层循环i枚举当前一共遇到的店的数目,第二层循环j枚举一共遇到的花的数目,第三层循环k枚举当前有多少斗酒,注意在状态转移的时候需要判断上一个状态是否合法,上一个状态合法才可以转移到当前状态,因为最终需要将酒喝完所以酒的数目一定小于等于M,所以第三层循环长度为M,状态表示和状态计算其中可以参照下图:

package main

import "fmt"

func main() {
	var (
		n, m int
	)
	fmt.Scan(&n, &m)
	const (
		N   = 110
		mod = 1000000007
	)
	var f [N][N][N]int
    // 初始的合法状态
	f[0][0][2] = 1
	for i := 0; i <= n; i++ {
		for j := 0; j <= m; j++ {
			for k := 0; k <= m; k++ {
                // i >= 1 && k%2 == 0判断上一个状态是否合法
				if i >= 1 && k%2 == 0 {
					f[i][j][k] = (f[i][j][k] + f[i-1][j][k/2]) % mod
				}
				if j >= 1 {
					f[i][j][k] = (f[i][j][k] + f[i][j-1][k+1]) % mod
				}
			}
		}
	}
	fmt.Println(f[n][m-1][1])
}
class Solution:
    def process(self):
        n, m = map(int, input().split())
        N, mod = 110, 10 ** 9 + 7
        f = [[[0] * N for i in range(N)] for j in range(N)]
        f[0][0][2] = 1
        for i in range(n + 1):
            for j in range(m + 1):
                for k in range(m + 1):
                    if i >= 1 and k % 2 == 0:
                        f[i][j][k] = (f[i][j][k] + f[i - 1][j][k // 2]) % mod
                    if j >= 1:
                        f[i][j][k] = (f[i][j][k] + f[i][j - 1][k + 1]) % mod
        print(f[n][m - 1][1])


if __name__ == "__main__":
    Solution().process()

砍竹子(思维题)

这天,小明在砍竹子,他面前有 n 棵竹子排成一排,一开始第 i 棵竹子的高度为 hi. 他觉得一棵一棵砍太慢了,决定使用魔法来砍竹子。魔法可以对连续的一段相同高度的竹子使用,假设这一段竹子的高度为 H,那么使用一次魔法可以把这一段竹子的高度都变为 ⌊√⌊H /2⌋ + 1⌋,其中 ⌊x⌋ 表示对 x 向下取整。小明想知道他最少使用多少次魔法可以让所有的竹子的高度都变为 1。

思路分析:从直觉上感觉对于高度为hi的竹子操作的次数应该是比较少的,我们可以输出高度为10 ^ 18的竹子看需要多少次的操作,hi  = 10 ^ 18的时候只需要6次就可以将高度变为1,对于每一根竹子使用魔法操作将高度变为1的时候有对应的操作次数,我们可以将每一次操作看成是一层,当相邻两个高度相同的时候可以一起操作,基于这个思想我们可以预处理出所有的竹子,将通过魔法操作得到的高度存储在f中,f[i][j]表示第i根竹子的第j层的高度,最后枚举每一层,对于相邻两根竹子对应层的高度相同那么操作次数减1即可(在使用魔法操作的时候累加所有的操作次数):

go语言(超时):使用 fmt.Scan() 函数读取超过 10 ^ 5 的数据就会很慢,需要使用 bufio.NewReader(), bufio.NewWriter(),fmt.Fscan(),fmt.Fprintln() 函数优化读取和输出数据:

package main

import (
	"fmt"
	"math"
)

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func main() {
	const N = 200010
	var (
		n   int
		stk [10]int
		f   [N][10]int
	)
	fmt.Scan(&n)
	m, res := 0, 0
	for i := 0; i < n; i++ {
		var (
			x int
		)
		fmt.Scan(&x)
		top := 0
		for x > 1 {
			top += 1
			stk[top] = x
			x = int(math.Sqrt(float64(x/2 + 1)))
		}
		res += top
		m = max(m, top)
		k := top
		for j := 0; j < top; j++ {
			f[i][j] = stk[k]
			k -= 1
		}
	}
	// 枚举每一层
	for i := 0; i < m; i++ {
		for j := 1; j < n; j++ {
			if f[j][i] != 0 && f[j][i] == f[j-1][i] {
				res -= 1
			}
		}
	}
	fmt.Println(res)
}

使用 bufio.NewReader() ,bufio.NewWriter() 和 fmt.Fscan() ,fmt.Fprintln() 函数优化输入输出数据:

package main

import (
	"bufio"
	"fmt"
	"io"
	"math"
	"os"
)

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func run(r io.Reader, w io.Writer) {
	in := bufio.NewReader(r)
	out := bufio.NewWriter(w)
	defer out.Flush()
	const N = 200010
	var (
		n   int
		stk [10]int
		f   [N][10]int
	)
	fmt.Fscan(in, &n)
	m, res := 0, 0
	for i := 0; i < n; i++ {
		var (
			x int
		)
		fmt.Fscan(in, &x)
		top := 0
		for x > 1 {
			top += 1
			stk[top] = x
			x = int(math.Sqrt(float64(x/2 + 1)))
		}
		res += top
		m = max(m, top)
		k := top
		for j := 0; j < top; j++ {
			f[i][j] = stk[k]
			k -= 1
		}
	}
	// 枚举每一层
	for i := 0; i < m; i++ {
		for j := 1; j < n; j++ {
			if f[j][i] != 0 && f[j][i] == f[j-1][i] {
				res -= 1
			}
		}
	}
	fmt.Fprintln(out, res)
}

func main() {
	run(os.Stdin, os.Stdout)
}

python:

import math


class Solution:
    def process(self):
        n = int(input())
        h = list(map(int, input().split()))
        f = [[0] * 10 for i in range(n + 10)]
        stk = [0] * 10
        res = m = 0
        for i in range(n):
            top = 0
            x = h[i]
            while x > 1:
                top += 1
                stk[top] = x
                x = int(math.sqrt(x // 2 + 1))
            m = max(m, top)
            res += top
            k = top
            for j in range(top):
                # 逆序存储每一层的高度
                f[i][j] = stk[k]
                k -= 1
        for i in range(m):
            for j in range(1, n):
                if f[j][i] and f[j][i] == f[j - 1][i]:
                    res -= 1
        print(res)


if __name__ == '__main__':
    Solution().process()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值