今天在牛客网上刷剑指offer,遇到一个跟上篇博客有相似之处的算法题。先附上上一篇博客的链接。
剑指offer——输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
题目描述
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?
方法一:
咋一看好像是有点类似,共同点:递增正数列、和一定。(emmmmm.... 那有怎样呢?)
此时你可能会问:“之前的那道题不是才两个数吗? 这道题你怎么知道有几个数”,的确,这道题是正数序列,但是这题的正数序列有一个明显特征:连续。连续的正数,也是公差为1的等差数列呗,这样我们就可以将连续数列的和转换为两个数的和,S=(首项+尾项)*项数/2=(low+high)*(high-low+1)/2;这样我们就将多个数的和,转换为只跟首尾两个数有关了,那我们怎么确定首尾呢?我们之前的夹逼法,在判断两个数的和与S之间的大小关系的时候,若大于S,high--,r若小于S,low++;那这道题是否也采用夹逼的方法进行解决呢?很明显不可以,为什么呢?因为当low+high>S时,让low+high更接近S的方法,只可能是high--;但是这道题,当low+high>S时,正数数列的和更接近S的方法有两个:high--或low++; 在与S比较的时候,不能确定是high--还是low++。所以夹逼方法在这是不可行的。
此时我们应该转换一下思路,在两个数相加的的时候,我们是通过high--或low++的方式,不管是high--还是low++,都是在最小化的逼近S。那么,我们在正数序列中元素的个数不确定的情况下,如何让每一次与S判断之和的操作更加逼近S呢?
只能从最小的正数序列开始,也就是{1,2},即low=1,high=2;判断a=(low+high)*(high-low+1)/2是否与S相等,若a<S,则为了变化最小,且要接近S,只能high++;若a>S,只能low++;
代码如下:
/**
* @param sum
* @return 输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
*/
private static ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> list = new ArrayList<ArrayList<Integer>>();
int low = 1,high =2 ; //从最小的正整数序列开始
int res = 0;
while (low < high) {
res = (low+high)*(high-low+1)/2; //根据数列求和公式,求以low、high为首尾项数列的和
if (res == sum) {
ArrayList<Integer> arrayList = new ArrayList<Integer>();
for (int i = low; i <= high; i++) {
arrayList.add(i);
}
list.add(arrayList);
high++; //为了输出所有的结果,在这做low++或high++操作,使得首尾指针发生改变,求出其他满足条件的情况。
}else if (res < sum) {
high++;
}else
{
low++;
}
}
return list;
}
无论是求两个和为S的两个数,还是求和为S的正数序列,共同的思路都是转换为首尾指针的变化,通过首尾指针的变化来使和缓慢接近S,直到相等。
方法二
第二种方法是根据本题的特征加上数学的方法得到的。观察可以很容易得到,当正数序列的个数为奇数或偶数时,都会有其特征。当序列个数为奇数时,很明显18,19,20,21,22序列中,中位数与平均数相等,也就是说当序列个数为奇数时,只需要知道序列的个数就可以得到完整序列。当序列为偶数时,中间两个数相加的和乘以项数就等于S;
根据以上,方法也可以得到正确的解答,代码如下:
private static ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
ArrayList<ArrayList<Integer>> list = new ArrayList<ArrayList<Integer>>();
for (int i = sum / 2 + 1; i > 1; i--) {
//i表示正数序列的个数
if (i % 2 == 0) { //序列元素个数为偶数个时
int mid= sum / i; //[9, 10, 11, 12, 13, 14, 15, 16] ,mid就相当于 12
int temp = i / 2; // 项数为4
//中间两项相加*项数,若不等于sum,则个数有误,直接continue.
if (mid < temp || (mid+mid+1)*temp != sum) {
continue;
}
ArrayList<Integer> arrayList = new ArrayList<Integer>();
//j的初始值,也就是首项不能小于0,在上面已经作校验
for (int j = mid - temp + 1; j <= mid+temp; j++) {
arrayList.add(j);
}
list.add(arrayList);
}else
{
if (sum % i !=0) { //当个数为奇数,平均值不为整数,说明个数有误,直接continue
continue;
}
else
{
int mid = sum / i;
int temp = i / 2;
if (mid <= temp) {
continue;
}
ArrayList<Integer> arrayList = new ArrayList<Integer>();
for (int j = mid - temp; j<= mid+temp; j++) {
if (j==0) {
System.out.println(mid+","+temp);
}
arrayList.add(j);
}
list.add(arrayList);
}
}
}
return list;
}
可能大部分人第一想到的就是方法二吧,纯数学思想,也很简单,但是要考虑到边界条件,不然会有bug。