2020_java蓝桥_暑假训练_分治与动态规划2

🌸分治与动态规划训练

贴在前面,想了解什么是分治,可查看我的这篇文章二分查找

🍁大数相乘

用串的形式表示大数的乘法。
即求类似:
“23234845847839461464158174814792” * “6457847285617487843234535”
要求结果返回一个串。

🍂分析

这道题既然写在分治里,那必然跟分治脱不了干系,当然一开始也还是想不到分治是怎么分治的(确实好难)。所谓万物皆暴力,一切能暴力出来就先暴力出来,在进行一步步优化升级。好在这道题没有要求不能使用乘法,要是那样的话就整个人都不好了

💥利用大数类BigDecimal
public class String_Multiple {
	public static void main(String[] args) {
		System.out.println(multiply("23234845847839461464158174814792", "6457847285617487843234535"));
	}
	/**
	 * 利用大数类
	 * @param num1
	 * @param num2
	 * @return
	 */
	private static String multiply(String num1, String num2) {
		return new BigDecimal(num1).multiply(new BigDecimal(num2)).toString();
	}
}

💥模拟人工乘法

人工乘法在座的各位没有谁不会吧???手写的过程利用计算机去一步步实现罢了(对于大数不建议使用,复杂度较高)

public class String_Multiple {
	public static void main(String[] args) {
		System.out.println(multiply1("23234845847839461464158174814792", "6457847285617487843234535"));
	}
	private static String multiply1(String num1, String num2) {
        if (num1.equals("0") || num2.equals("0")) {
            return "0";
        }
        // 保存计算结果
        String res = "0";
        
        // num2 逐位与 num1 相乘
        for (int i = num2.length() - 1; i >= 0; i--) {
            int carry = 0;
            // 保存 num2 第i位数字与 num1 相乘的结果
            StringBuilder temp = new StringBuilder();
            // 补 0 
            for (int j = 0; j < num2.length() - 1 - i; j++) {
                temp.append(0);
            }
            int n2 = num2.charAt(i) - '0';
            
            // num2 的第 i 位数字 n2 与 num1 相乘
            for (int j = num1.length() - 1; j >= 0 || carry != 0; j--) {
                int n1 = j < 0 ? 0 : num1.charAt(j) - '0';
                int product = (n1 * n2 + carry) % 10;
                temp.append(product);
                carry = (n1 * n2 + carry) / 10;
            }
            // 将当前结果与新计算的结果求和作为新的结果
            res = addStrings(res, temp.reverse().toString());
        }
        return res;
	}
    /**
     * 对两个字符串数字进行相加,返回字符串形式的和
     */
    private static String addStrings(String num1, String num2) {
        StringBuilder builder = new StringBuilder();
        int carry = 0;
        for (int i = num1.length() - 1, j = num2.length() - 1;
             i >= 0 || j >= 0 || carry != 0;
             i--, j--) {
            int x = i < 0 ? 0 : num1.charAt(i) - '0';
            int y = j < 0 ? 0 : num2.charAt(j) - '0';
            int sum = (x + y + carry) % 10;
            builder.append(sum);
            carry = (x + y + carry) / 10;
        }
        return builder.reverse().toString();
    }
}

💥分治

对于大整数的乘法,我们可以利用分治法将其两个字符串从中间截断,将两字符串的后半部分,进行相乘

子串a1和子串a2,然后结果就是a1+0…000+a2。递归的边界条件就是当每个子串的长度都小于等于4的时候,就可以通过直接乘法运算返回结果。

大数乘法中必须用到大数加法(这里不用上面已经实现的字符串相加函数),而大数加法的思路和大数乘法也是大同小异的,将串a和串b的末尾都切出一段长度为8的子串出来,a从左到右子串分别是a1和a2,b亦然。之后就是考虑t=a2+b2的不同情况,有可能t.size<8,那么这个时候我们就要通过补0的方式)凑够8位。

public class String_Multiple {
	public static void main(String[] args) {
		System.out.println(multiply2("23234845847839461464158174814792", "6457847285617487843234535"));
	}
    private static String multiply2(String num1, String num2) {
    	if(num1.length() <= 4 && num2.length() <= 4) {
    		return Integer.parseInt(num1) * Integer.parseInt(num2) + "";
    	}
    	if(num1.length() > 4) {
    		int mid = num1.length() / 2;
    		String s1 = num1.substring(0, mid);
    		String s2 = num1.substring(mid);
    		return add(multiply2(s1, num2) + add_zero(s2.length()), multiply2(s2, num2));
    	}
    	return multiply2(num2, num1);
    }
    //补0
    private static String add_zero(int x) {
    	if(x == 0) {
    		return "";
    	}
    	if(x == 1) {
    		return "0";
    	}
    	return add_zero(x / 2) + add_zero(x / 2) + add_zero(x % 2);
    }
    /**
    *大数相加
    */
    private static String add(String num1, String num2) {
    	if(num1.length() <= 8 && num2.length() <= 8) {
    		return Integer.parseInt(num1) + Integer.parseInt(num2) + "";
    	}
    	String a1 = "0";
    	String a2 = num1;
    	if(num1.length() > 8) { //将num1的低八位分离
    		a1 = num1.substring(0, num1.length() - 8);
    		a2 = num1.substring(num1.length() - 8);
    	}
    	String b1 = "0";
    	String b2 = num2;
    	if(num2.length() > 8) {
    		b1 = num2.substring(0, num2.length() - 8);
    		b2 = num2.substring(num2.length() - 8);
    	}
    	String k = add(a2, b2);
    	if(k.length() > 8) {
    		return add(add(a1, b1), "1") + k.substring(1);
    	}
    	if(k.length() < 8) {
    		return add(a1, b1) + add_zero(8 - k.length()) + k;
    	}
    	return add(a1, b1) + k;
    }
}

🍁城墙刷漆

X国的一段古城墙的顶端可以看成 2*N个格子组成的矩形(如图所示)

现需要把这些格子刷上保护漆。

你可以从任意一个格子刷起,刷完一格,可以移动到和它相邻的格子(对角相邻也算数),但不能移动到较远的格子(因为油漆未干不能踩!)

比如:a d b c e f 就是合格的刷漆顺序。

c e f d a b 是另一种合适的方案。

当已知 N 时,求总的方案数。当N较大时,结果会迅速增大,请把结果对 1000000007 (十亿零七) 取模。

输入数据为一个正整数(不大于1000)

输出数据为一个正整数。

例如:
用户输入:
2
程序应该输出:
24 
再例如:
用户输入:
3
程序应该输出:
96
再例如:
户输入:
22
程序应该输出:
359635897
🍂分析

本题的任务是在已知几个基本行走规则的前提下,求解遍历整个矩形的行走路线数量
很多同学的第一想法是搜索,但是搜索算法必然超时,因为本题的数据范围(n最大可取到1000)会使得递归树过深,因此我们不得不另寻思路。而实际上,这种走格子的题目往往和动态规划密切相关
对于题目给出的几类行走规则,我们可以很容易地联想到递推。因为对于某个格子,其走到当前可能有很多种走法,但是从另一个角度看来,以某个格子为出发点进行“刷漆”,其行走方式却是一个固定值。我们首先要做的,就是来推理这个过程,从而找到动态转移方程。
为了将整个矩形刷完,我们的起点主要分为以下两大类:

  1. 从四个顶点之一出发
  2. 从中间某个点出发

(一)从顶点出发
① 第一步走同一列的另一个格子,然后再走下一列。接着重复这个过程。如下图所示:
从顶点A出发

比如:假设从顶点A出发,那么第一步没得选,只能走向B;接着在B点时,此时就有两种选择方案,要么走向C,要么走向D。假设走向了D,那么此时D点就只能选C,接着在C点时,其又可以选择E或F……然后重复上面这个过程,直到最终走到矩形的另一侧
这种情况(一趟过去,不再返回)用数组a来表示,则可以把问题规模由a[ i ]转换成a[ i-1]
于是得到状态转移方程:a[ i ]=2×a[ i-1](乘2的原因是每次都有两种选择,如在B点可选C或D)

② 第一步走下一列,之后也还是不断地走下一列,当最终走到最后一列后再返回。返回时,由于格子的高度为2,那么在返回时,路径唯一,如下图所示:
从顶点A出发

比如:假设从顶点A出发,那么第一步可以选C或D共两种方案,假设选的是C,那么接下来又可以选择E或F……当最后到了最后一列,比如到了I,那么此时返回的路线也就唯一确定了
这种情况(一趟过去,一趟回来)用数组b来表示,则可以把问题规模由b[i]转换成b[ i-1]
于是得到状态转移方程:b[ i ]=2×b[ i-1] (乘2的原因是每次都有两种选择,如在A点可选C或D)

③ 第一步走另一列,再由该列返回前一列,然后再从前一列走向另一列的另一个格子,如下图所示:
从顶点A出发

比如:假设从顶点A出发,那么其有两条路线:A->C->B->D或A->D->B->C,假设到了点D,则D又可以有两种选择(要么到E要么到F),此时又可再重复在点A的行为,直到最终到达矩形的另一侧
显然这也属于“一趟过去,不再返回”的类型,因此也用数组a来表示,则可以把问题规模转换成a[ i-2 ]
于是得到状态转移方程:a[ i ] = 2 × 2 × a[ i-2 ] (第一个乘2是选C或D,二个乘2是选E或F)

综上便分析出了所有的从四个顶点出发的基础遍历方式
于是得到转移方程:a[ i ] = 2 × a[ i-1] + b[ i ] + 2 × 2 × a[ i-2]
由于顶点有4个,于是最终的遍历方案为:sum=4×a[ i ]


(二)从中间出发
从中间出发
假设我们从图中i=3(E点)处出发(以i为分割线,将图分为左边的ABCDEF以及右边的GHIJ),为了遍历所有格子,我们需要先走完左边的这个整体(特别注意:这里必须倒回到F才能继续走右边),然后再把右边视为以G或者H为起点的一组格子将其走完(因此这里需要乘以2,两种起点出发嘛)
分析上述的流程,可以得到从中间出发的方案数为:( b[ i ] ) × ( 2 × a[ n-i ] )
同理,我们可以先走右边的EFGHIJ,然后再走左边的ABCD
这样的方案数为:(b[ n-i+1]) × (2×a[ i - 1])
由于上面的所有起始点都是以E为出发点行走的,我们同理也可以以F为起点出发,那么从第i列开始刷漆的方法就有:[ (b[ i ])×(2×a[ n-i ])+(b[ n-i+1])×(2×a[ i - 1]) ]×2

综合一二,便得出最终的方案数sum为:sum = 4×a[n] + 2×2×( b[ i ]×a[ n-i ] + b[ n-i+1 ]×a[ i - 1] )
这便是本题的递推公式了,现在的问题是,初值呢?
由于在(一)中存在公式:a[ i ] = 2×2×a[i-2],故我们需要将a[1]、a[2]、b[1]、b[2]都给定
所以下面我们需要手动地给出i=1和2的情况:

a[1]=1;		//n=1时,从顶点出发显然只有一种情况
a[2]=6;		//n=2时,从顶点出发有三种情况:a[2]=2+2+2,分别是(一)中的①②③

情况①:
情况①

情况②:
情况②

情况③:
情况③

b[2]=2; 	//显然这种情况下只能是两种,对应上图的情况②
b[1]=1;		//b这种刷漆方式至少需要两个格子(即最低i=2),所以b[1]严格意义上说来是0,但是由于b[2]=2,且存在公式b[i]=2*b[i-1],故这里反推b[1]=1

题解出自酱懵静

public class Lattice_brush_paint {
	private static final int MOD = 1000000007;
	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		int n = input.nextInt();
		System.out.println(brush(n));
		input.close();
	}
	private static int brush(int n) {
		if(n == 1) {
			return 2;
		}
		long a[] = new long[1005];
		long[] b = new long[1005];
		a[1] = 1;
		a[2] = 6;
		b[1] = 1;
		b[2] = 2;
		for(int i = 3; i <= n; i++) {  //从一个顶点出发
			b[i] = (2 * b[i - 1]) % MOD;	//边操作边取余
			a[i] = (2 * a[i - 1] + b[i] + 4 * a[i - 2]) % MOD;
		}
		long sum = 4 * a[n] % MOD;	//4个顶点
		for(int i = 2; i < n; i++) {//从中间出发,注意i的范围是大于1小于n 
			sum = (sum + 4 * (b[i] * a[n - i] + b[n - i + 1] * a[i - 1])) % MOD;
		}
		return (int)sum;
	}
}

🍁环形涂色

如图,组成环形的格子需要涂3种颜色。

它们的编号分别是1~14

相邻的格子不能用相同的颜色。

涂色方案的数目是:24576

当格子数目为50的时候,求涂色方案总数。

dtcNNT.png

🍂dp分析
  • 假设现在考虑最后一个格子填入什么颜色,如果它的前一个格子的颜色和第一个格子的颜色不一样,那么它有两种填法,此时,再看它前面的前面的格子如何填,因为这个格子的后面格子颜色和第一个格子颜色相同,所以相当于之前情况的一个缩小版,有f(n-2)\*2种,

  • *再考虑最后一个格子的前一个格子的颜色和第一个格子的颜色一样,那么它有一种填法,而现在再来考虑这个格子之前应该如何涂颜色,也相当于之前情况的缩小版,于是有f(n-1)\*1种。

public class Ring_Painting {
	public static void main(String[] args) {
		Scanner input = new Scanner(System.in);
		int n = input.nextInt();
		System.out.println(paint(n));
		input.close();
	}
	private static long paint(int n) {
		long[] dp = new long[n + 1];
		dp[1] = 3;
		dp[2] = 6;
		dp[3] = 6;
		for(int i = 4; i <= n; i++) {
			dp[i] = dp[i - 1] + 2 * dp[i - 2];
		}
		return dp[n];
	}
}

最后,不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值