波动数列

问题描述

观察这个数列:
  1 3 0 2 -1 1 -2 …

这个数列中后一项总是比前一项增加2或者减少3。

栋栋对这种数列很好奇,他想知道长度为 n 和为 s 而且后一项总是比前一项增加a或者减少b的整数数列可能有多少种呢?
输入格式
  输入的第一行包含四个整数 n s a b,含义如前面说述。
输出格式
  输出一行,包含一个整数,表示满足条件的方案数。由于这个数很大,请输出方案数除以100000007的余数。
  
样例输入
4 10 2 3
样例输出
2
样例说明
  这两个数列分别是2 4 1 3和7 4 1 -2。
  
数据规模和约定
  对于10%的数据,1<=n<=5,0<=s<=5,1<=a,b<=5;
  对于30%的数据,1<=n<=30,0<=s<=30,1<=a,b<=30;
  对于50%的数据,1<=n<=50,0<=s<=50,1<=a,b<=50;
  对于70%的数据,1<=n<=100,0<=s<=500,1<=a, b<=50;
  对于100%的数据,1<=n<=1000,-1,000,000,000<=s<=1,000,000,000,1<=a, b<=1,000,000。

java代码如下:

首先暴力破解,求得可能的最小的首项与可能的最大首项,然后dfs遍历所有可能。
该代码同时输出了可能的解,使用注释掉的代码即可只输出可能情况数。
但是效率低,不能通过所有测试用例。

import java.math.BigInteger;
import java.util.*;

public class Main {

	static int n = 0;
	static int s = 0;
	static int a = 0;
	static int b = 0;
	static BigInteger ans = new BigInteger("0");
	static int[] path;

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		s = sc.nextInt();
		a = sc.nextInt();
		b = sc.nextInt();
		path = new int[n];
		//得到可能的首项的最小值与最大值
		int min = (2 * s - a * n * (n - 1)) / (2 * n);
		int max = (2 * s + b * n * (n - 1)) / (2 * n);
		for (int x = min; x <= max; x++) {
		//	dfs(x, x, 0);
			dfs(x, x, 0, path);
		}
		ans = ans.remainder(new BigInteger("100000007"));
		System.out.println(ans);
		sc.close();
	}

	/**
	 * 计算以x为首项的所有可能序列和是否为s,是则ans++
	 * 
	 * @param x
	 *            上一项的值
	 * @param sum
	 *            截止上一项的和
	 * @param i
	 *            深度,也即求了多少项
	 */
	private static void dfs(int x, int sum, int i) {
		if (i == (n - 1)) {
			if (sum == s) {
				ans = ans.add(new BigInteger(("1")));
			}
			return;
		}

		dfs(x + a, sum + (x + a), i + 1);
		dfs(x - b, sum + (x - b), i + 1);
	}
	//相比较上一个dfs多了path用来记录并输出数列
	private static void dfs(int x, int sum, int i, int[] path) {
		if (i == (n - 1)) {
			if (sum == s) {
				ans = ans.add(new BigInteger(("1")));
				path[i] = x;
				for (int j = 0; j < path.length; j++) {
					System.out.print(path[j] + " ");
				}
				System.out.println();
			}
			return;
		}
		path[i] = x;
		dfs(x + a, sum + (x + a), i + 1, path);
		dfs(x - b, sum + (x - b), i + 1, path);

	}
}

第一次优化的java代码:

观察发现,无论数列的每一次是+a还是-b,总的数列中a与b的个数和一定为 n * (n-1) / 2。
那么对于每一个[min, max]中的x,首先让a的个数从0开始递增,同时计算出b的个数,再算出 n*x + countOfA * a - countOfB * b的值,将其与s比较,若相等,则以x为首项的数列有可能构成可行解,但是可能有多种不同的满足要求的数列,此时再dfs得到题目要求解。

import java.math.BigInteger;
import java.util.*;

public class Main {

	static int n = 0;
	static int s = 0;
	static int a = 0;
	static int b = 0;
	static BigInteger ans = new BigInteger("0");
	static int[] path;

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		s = sc.nextInt();
		a = sc.nextInt();
		b = sc.nextInt();
		path = new int[n];
		// 得到可能的首项的最小值与最大值
		int min = (2 * s - a * n * (n - 1)) / (2 * n);
		int max = (2 * s + b * n * (n - 1)) / (2 * n);
		// 在前n项的和中,共有n*(n-1)/2个a和b
		// 对于min和max中的所有x
		for (int x = min; x <= max; x++) {
			for (int cntOfA = 0; cntOfA <= n * (n - 1) / 2; cntOfA++) {
				long cntOfB = n * (n - 1) / 2 - cntOfA;
				// 如果n个x和cntOfA个a和cntOfB个b的和为s,再寻找该数列有多少种可能的排列
				// 需要满足后一个是前一个的+a或-b
				if ((n * x + cntOfA * a - cntOfB * b) == s) {
					// dfs(x, x, 0, path);
					dfs(x, x, 0);
				}
			}
		}
		ans = ans.remainder(new BigInteger("100000007"));
		System.out.println(ans);
		sc.close();
	}

	/**
	 * 计算以x为首项的所有可能序列和是否为s,是则ans++
	 * 
	 * @param x
	 *            上一项的值
	 * @param sum
	 *            截止上一项的和
	 * @param i
	 *            深度,也即求了多少项
	 */
	private static void dfs(int x, int sum, int i) {
		if (i == (n - 1)) {
			if (sum == s) {
				ans = ans.add(new BigInteger(("1")));
			}
			return;
		}

		dfs(x + a, sum + (x + a), i + 1);
		dfs(x - b, sum + (x - b), i + 1);
	}

	//相比较上一个dfs多了path用来记录并输出数列
	private static void dfs(int x, int sum, int i, int[] path) {
		if (i == (n - 1)) {
			if (sum == s) {
				ans = ans.add(new BigInteger(("1")));
				path[i] = x;
				for (int j = 0; j < path.length; j++) {
					System.out.print(path[j] + " ");
				}
				System.out.println();
			}
			return;
		}
		path[i] = x;
		dfs(x + a, sum + (x + a), i + 1, path);
		dfs(x - b, sum + (x - b), i + 1, path);

	}
}

第二次代码优化

之前的dfs对于x的值从min到max遍历处理了一次,能否只处理一部分呢?
因为n* x+countOfA* a-countOfB* b=s
则有当(s-countOfA* a+countOfB* b)%n为0时,可得一种可能的x。
此外,能否由countOfA的值直接得到满足条件的情况数呢?-显然countOfA是从0,1,2,……,n-1中选择若干个数相加求和得到,则该问题转化为0-1背包问题。

import java.util.*;

public class Main {

	static int n = 0;
	static int s = 0;
	static int a = 0;
	static int b = 0;
	static int ans = 0;
	static final int MOD = 100000007;
	static int[] path;

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		s = sc.nextInt();
		a = sc.nextInt();
		b = sc.nextInt();
		dp1();
		System.out.println(ans);
		sc.close();
	}

	static void dp1() {
		int t = n * (n - 1) / 2; // countOfA的最大取值
		int[][] dp = new int[n][t + 1]; // dp存放前i个数组合相加得到j的组合方法数
		// 初始化dp
		for (int i = 0; i < dp.length; i++) {
			dp[i][0] = 1;
		}
		// 填表
		for (int i = 1; i < dp.length; i++) {
			for (int j = 1; j < dp[i].length; j++) {
				if (i > j) {
					dp[i][j] = dp[i - 1][j];
				} else {
					// 注意在这里dp[i][j]要直接对MOD取余,否则可能会因为数值过大溢出导致错误
					dp[i][j] = (dp[i - 1][j] + dp[i - 1][j - i]) % MOD;
				}
			}
		}
		for (int countOfA = 0; countOfA <= t; countOfA++) {
			int countOfB = t - countOfA;
			if ((s - countOfA * a + countOfB * b) % n == 0) {
				// 此处注意取模
				ans = (ans += dp[n - 1][countOfA]) % MOD;
			}
		}
	}
}

第三次代码优化

如上代码可得70分,但是还有三种情况内存溢出,接下来考虑空间效率优化。
容易发现,dp数组第i行的数据只需要知道i-1行的数据,则可将原数据压缩成2行。
在dp内二重循环中,当j大于1,2,……,i-1的和时,dp[i][j]显然为0,可以利用该特点减少枚举。
dp的最后一个循环中的if判断内可能会溢出int类型,将相关变量修改为long。

package test;

import java.util.*;

public class Main {

	static int n = 0;
	static int s = 0;
	static int a = 0;
	static int b = 0;
	static int ans = 0;
	static final int MOD = 100000007;
	static int[] path;

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		n = sc.nextInt();
		s = sc.nextInt();
		a = sc.nextInt();
		b = sc.nextInt();
		dp();
		System.out.println(ans);
		sc.close();
	}

	static void dp() {
		int t = n * (n - 1) / 2; // countOfA的最大取值
		int[][] dp = new int[2][t + 1]; // dp存放前i个数组合相加得到j的组合方法数
		// 初始化dp数组
		dp[0][0] = 1;
		dp[1][0] = 1;
		// 填表
		// 利用row实现2行的滚动数组
		int row = 0;
		for (int i = 1; i < n; i++) {
			row = 1 - row;
			// 当j大于1,2,……,i-1的和时,dp[i][j]显然为0,可以减少枚举
			for (int j = 1; j <= i * (i + 1) / 2; j++) {
				if (i > j) {
					dp[row][j] = dp[1 - row][j];
				} else {
					// 注意在这里要直接对MOD取余,否则可能会因为数值过大溢出导致错误
					dp[row][j] = (dp[1 - row][j] + dp[1 - row][j - i]) % MOD;
				}
			}
		}
		for (long countOfA = 0; countOfA <= t; countOfA++) {
			long countOfB = t - countOfA;
			//下一行的运算结果可能溢出,将countOfA改为long类型,则如下运算结果自动转化为long,不会溢出
			if ((s - countOfA * a + countOfB * b) % n == 0) {
				// 此处注意取模
				ans = (ans += dp[row][(int)countOfA]) % MOD;
			}
		}
	}
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值