问题描述
观察这个数列:
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;
}
}
}
}