子集和问题:给定一组数和一个值,从这组数中选出若干个数使其和为给定的值。这是个NPC问题。
1、https://leetcode.com/problems/counting-bits/#/solutions
给定一非负Integer num,求[0,num]每个数的二进制形式中1的个数 f[num+1]。
解法:可用最朴素的方法逐个求,但其实有规律: f[i] = f[i / 2] + i % 2 或 f[i] = f[i&(i-1)] + 1;
2、Single Number:一组Integer类型的数,除了一个出现N次外,其他都出现M次,找出此数(N ≠ M)。(相关题目:https://leetcode.com/problems/single-number-ii/#/description)
法1:排序,时间复杂度最少O(nlgn)
法2:利用哈希表计每个数出现此时,时间复杂度近似O(n),空间复杂度为O(n)
法3:(推荐)思路:每个Integer32位,对于每一位,各个数该位上的值的和对M求模,结果即为特殊的数在该位上的和的N倍(若N>M则为 N%M倍)。时间复杂度为O(n),空间复杂度为O(1)
代码如下:
1 public int singleNumber(int[] nums, int M, int N) {// with constant memory 2 int res = 0; 3 int tmpCurBitSum = 0; 4 for (int bit = 0; bit < 32; bit++) { 5 tmpCurBitSum = 0; 6 for (int i : nums) { 7 tmpCurBitSum += ((1 << bit) & i) >>> bit; 8 } 9 res |= ((tmpCurBitSum % M)/(N%M)) << bit; 10 } 11 return res; 12 }
特殊情况:M为偶数,N为奇数,如当M=2,N=1时候即为经典的题“除了一个数出现一次外其他数都出现2次”,此时还可以用更简单的方法:所有数异或,结果即为特殊数。
题目变换:有两个分别出现奇数次,其他的都出现偶数次。仍可以用法1或法2解决,但空间复杂度高;另法:所有数异或得到特殊的那两个数的异或值a,对于a中值为1的位,在原两数中该位上的值肯定不同,故可选一位将所有数分成两组,两特殊数分别在两组中,两组各自异或得到两个数即为结果。(相关题目:https://leetcode.com/problems/single-number-iii/#/solutions)
3、String to Integer(atoi)
逐个字符处理,res=res*10+ ch-'0';,但是需要判断溢出,细节处理有点麻烦。可以换个思路判断:res> (Integer.MAX_VALUE-(ch-'0'))/10 时即溢出,这样可以省很多细节。
相关题目:https://leetcode.com/problems/string-to-integer-atoi/#/description
4、子段和问题:(递推关系设计:连续子串——包含边界元素时,子序列——不要求包含边界元素)
4.1、字段和的最大值
设有序列 a[1,2,...,n]
1、最大子段和(一个子段):设 b[j] 为 a[1,2,...,j] 包含 a[j] 的最大子段和,则根据b[j-1]是否大于0有: b[j] = max{a[j], b[j-1]+a[j] }
1 int maxSum(int n,int *a) 2 { 3 int sum=0,b=0; 4 for(int i=0;i<n;i++) 5 { 6 if(b>0) b+=a[i]; 7 else b=a[i]; 8 if(sum<b) sum=b; 9 } 10 return sum; 11 }
2、最大m子段和(找出m个子段,可以相邻):设 b[i,j] 为 a[1,2,...,j] 前j项中i个子段的最大和且第i个子段包含a[j](1≤i≤m, i≤j≤n),则根据第i个子段是否仅仅包含a[j]有:
b[i,j]= 0 (i=0或j=0时)
b[i-1,j-1]+a[j] (i==j时)
max{ b[i,j-1]+a[j], (max b[i-1,t])+a[j] } (i<j时),其中i≤t≤j-1
3、最大不相邻子段和(找出任意个子段,但子段不能相邻):设 b[j] 为 a[1,2,...,j] 不相邻子段和的最大值(不要求包括a[j]),则根据 b[j] 是否包括 a[j] 有:b[j]= max{ a[j]+b[j-2], b[j-1] }。相关:LeetCode198:House Robber
1 public int maxNonadjacentSun(int[] nums) { 2 if(nums.length==0)return 0; 3 if(nums.length==1)return nums[0]; 4 5 int b1=0,b2=nums[0],tmp; 6 for(int i=1;i<nums.length;i++) 7 { 8 tmp=b1+nums[i]; 9 b1=b2; 10 if(b2<tmp) 11 { 12 b2=tmp; 13 } 14 } 15 return b2; 16 }
对于第一、第三种,由于b[j]只和前一个或两个状态有关,因此可以不设数组b[]而是采用数个变量来求;对于第二种同理可以只用两个数组来实现。
4.2、子段和的种数
设有序列 a[1,2,...,n],给定数sum
1、求序列中两数和等于sum的种数:LeetCode1:TwoSum
2、求序列中若干个数和等于sum的种数:设dp[i,j]为从序列前i个数中选若干个使得和为j的种数,则:
dp[i,j]=
1,i=0且j=0时;
0,i=0且j=1,2,...sum时;
dp[i-1,j],a[i]>j时;
dp[i-1,j] + dp[ i-1,j-a[i] ],a[i]≤j时
1 public static int resolve(int[] a, int sum) {// num of solutions that addup to sum 2 if (a == null || a.length == 0) { 3 return 0; 4 } 5 int n = a.length; 6 int[][] dp = new int[n + 1][sum + 1]; 7 dp[0][0] = 1; 8 // dp[i][k]=0,k>0,默认被初始化了 9 for (int i = 1; i <= n; i++) { 10 for (int j = 0; j <= sum; j++) { 11 if (a[i - 1] > j) { 12 dp[i][j] = dp[i - 1][j]; 13 } else { 14 dp[i][j] = dp[i - 1][j] + dp[i - 1][j - a[i - 1]]; 15 } 16 System.out.printf("(%d,%d):%d\n", i, j, dp[i][j]); 17 } 18 } 19 return dp[n][sum]; 20 }
4.3、推广:最大/最小连续子段积
设有序列 a[1,2,...,n],设f(k)为a[1,2,...k]中包含a[k]元素的最大连续子数组积,相应的g(k)为包含a[k]的最小连续子数组积,
则 f(k) = max( f(k-1) * A[k], A[k], g(k-1) * A[k] ), g(k) = min( g(k-1) * A[k], A[k], f(k-1) * A[k] ) ,从而易在O(n)内求之。
5、最长公共子序列、最长公共子串、最长回文子序列、最长回文子串
5.1、最长公共子序列:设 dp[i,j] 为序列a1[1,2,...,i ]、a2[1,2,...j ] 的最长公共子序列的长度,则dp[i,j]= (a1[i]==a2[j])? (dp[i-1,j-1]+1): max{ dp[i-1,j], dp[i,j-1] }
5.2、最长公共子串:设 dp[i,j] 为序列a1[1,2,...,i ]、a2[1,2,...j ] 的包含未元素的最长公共子串的长度,则dp[i,j]= (a1[i]==a2[j])? (dp[i-1,j-1]+1): 0 }
1 private static int resolve(String s1, String s2) { 2 if (s1 == null || s2 == null) { 3 return 0; 4 } 5 int n = s1.length(); 6 int m = s2.length(); 7 int max = 0; 8 int[][] dp = new int[n + 1][m + 1]; 9 for (int i = 1; i <= n; i++) { 10 for (int j = 1; j <= m; j++) { 11 dp[i][j] = (s1.charAt(i - 1) == s2.charAt(j - 1)) ? (dp[i - 1][j - 1] + 1) : 0; 12 if (max < dp[i][j]) { 13 max = dp[i][j]; 14 } 15 } 16 } 17 return max; 18 }
5.3、最长回文子序列:https://leetcode.com/problems/longest-palindromic-subsequence/#/description
法1:即求字符串S与逆串的最长公共子序列
1 public int longestPalindromeSubseq1(String s) { 2 if(s==null || s.length()==0) return 0; 3 4 int len=s.length(); 5 int [][]c=new int[len+1][len+1]; 6 for(int i=0;i<c.length;i++) 7 { 8 c[0][i]=0; 9 c[i][0]=0; 10 } 11 12 for(int i=1;i<=len;i++) 13 { 14 for(int j=1;j<=len;j++) 15 { 16 if(s.charAt(i-1)==s.charAt(len-j)) 17 { 18 c[i][j]=c[i-1][j-1]+1; 19 } 20 else 21 { 22 c[i][j] = c[i-1][j]>c[i][j-1] ? c[i-1][j] : c[i][j-1]; 23 } 24 } 25 } 26 return c[len][len]; 27 }
法2:动态规划
设字符串为s,f(i,j)表示s[i..j]的最长回文子序列。 最长回文子序列长度为f(0, s.length()-1)
状态转移方程如下:
当i>j时,f(i,j)=0。
当i=j时,f(i,j)=1。
当i<j并且s[i]=s[j]时,f(i,j)=f(i+1,j-1)+2。
当i<j并且s[i]≠s[j]时,f(i,j)=max( f(i,j-1), f(i+1,j) )。
注:如果i+1=j并且s[i]=s[j]时,f(i,j)=f(i+1,j-1)+2=f(j,j-1)+2=2,这就是当i>j时f(i,j)=0的好处。
1 public int longestPalindromeSubseq(String s) { 2 if(s==null || s.length()==0) return 0; 3 4 int len=s.length(); 5 int [][]f=new int[len][len]; 6 7 for(int i=len-1;i>=0;i--)//由于递推式每次最多后移一行,因此从最后一行起 8 { 9 // for(int j=0;j<len;j++)//由于递推式每次最多前移一列,因此从第一列起。但由于创建f时元素自动初始化为0,所以这里可以从i起 10 for(int j=i;j<len;j++) 11 { 12 if(i>j) f[i][j]=0; 13 else if(i==j) f[i][j]=1; 14 else 15 {//i<j 16 if(s.charAt(i)==s.charAt(j)) f[i][j]=f[i+1][j-1]+2; 17 else f[i][j]=Math.max(f[i+1][j],f[i][j-1]); 18 } 19 } 20 } 21 return f[0][len-1]; 22 }
5.4、最长回文子串:https://leetcode.com/problems/longest-palindromic-substring/#/description
//设dp[i,j]表示si,...,sj是否为回文子串,则dp[i,j]= dp[i+1,j-1] && (s[i]==s[j]), i≤j; 初始:dp[i,i]=true,dp[i,i+1]=s[i]==s[i+1]
//O(n2),O(n2)
1 public class Solution { 2 //最长回文子串和最长回文子序列不一样。。 3 //设dp[i,j]表示si,...,sj是否为回文子串,则dp[i,j]= dp[i+1,j-1] && (s[i]==s[j]), i≤j; 初始:dp[i,i]=true,dp[i,i+1]=s[i]==s[i+1] 4 //O(n2),O(n2) 5 public String longestPalindrome(String s) { 6 if(s==null || s.length()==0) return ""; 7 int len=s.length(); 8 9 boolean [][]dp=new boolean[len][len];//标记dp[i+1,j-1]即左下角是否为回文子串 10 int resI=0,resJ=0; 11 for(int i=len-1;i>=0;i--) 12 { 13 dp[i][i]=true;//dp[i,i]=true; 14 for(int j=i+1;j<len;j++) 15 { 16 dp[i][j]= (j==i+1)?(s.charAt(i)==s.charAt(j)):(dp[i+1][j-1] && (s.charAt(i)==s.charAt(j))); 17 if((dp[i][j]==true) && (resJ-resI+1 < j-i+1)) 18 { 19 resI=i; 20 resJ=j; 21 } 22 } 23 } 24 return s.substring(resI,resJ+1); 25 } 26 }
5.5、n! 中0的个数,直接求n!的值再数0的个数显然数据一大几乎不可能求得。转换方向:由于0由2*5产生(就算是4*5等产生,最终也是由2*5产生),所以n! 中0的个数="2*5"的个数=5的个数(因为一个数的因子中2的个数肯定比5的个数多)
求n! 中5的个数:由于n!=1*2*...*n,(详情参阅:n的阶乘末尾0的个数)
法1:穷举法,求1~n中每个数的因子5的个数
1 int fun1(int n) 2 { 3 int num = 0; 4 int i,j; 5 for (i = 5;i <= n;i += 5) 6 { 7 j = i; 8 while (j % 5 == 0) 9 { 10 num++; 11 j /= 5; 12 } 13 } 14 return num; 15 }
法2:Z = N/5 + N /(5*5) + N/(5*5*5),直到式子为0
1 int fun2(int n) 2 { 3 int num = 0; 4 5 while(n) 6 { 7 num += n / 5; 8 n = n / 5; 9 } 10 11 return num; 12 }
6、给定n、m,求使得 i*j 为完全平方数的序列 (i,j) 的个数,其中 i ∈[1,n]、j ∈[1,m]:
1 public static void main(String[] args) { 2 Scanner sc = new Scanner(System.in); 3 int res = 0; 4 int n = sc.nextInt(); 5 int m = sc.nextInt(); 6 // ssr(a,b)是整数 等价于sqrt(a*b)是整数 等价于a*b是完全平方数 7 // 暴力O(n*m)在大数据时超时,以下为O(n*sqrt(m))的方法 8 for (int i = 1; i <= n; i++) { 9 // 找到能整除i的最大的完全平方数s 10 int s = 1; 11 for (int x = 2; x * x <= i; x++) { 12 if (i % (x * x) == 0) { 13 s = x * x; 14 } 15 } 16 int r = i / s;// n去掉s因子后的结果 17 // 要使a*b是完全平方数,b需要因子r和一个完全平方数 18 for (int y = 1; y * y * r <= m; y++) { 19 res++; 20 } 21 } 22 System.out.println(res); 23 sc.close(); 24 }
7、给定一个由数字组成的字符串,求出其可能回复的所有IP地址。如"25525512110"对应的ip地址可以为[255,255,121,10, 255,255,12,110]
1 private static void split(long sVal, int[] segments, int segmentId, List<String> ips) {// split(Long.parseLong(str), new int[4], 3, ipStrs); 2 if (segmentId == 0) { 3 if (0 <= sVal && sVal <= 255) { 4 ips.add(sVal + "," + segments[1] + "," + segments[2] + "," + segments[3]); 5 } 6 } else { 7 int mod, segmentVal; 8 for (int exp = 1; exp <= 3; exp++) { 9 mod = (int) Math.pow(10, exp); 10 segmentVal = (int) (sVal % mod); 11 if (0 <= segmentVal && segmentVal <= 255) { 12 segments[segmentId] = segmentVal; 13 split(sVal / mod, segments, segmentId - 1, ips); 14 } 15 } 16 } 17 }
8、给定一个随机生成器,生成0和1的概率分别为0.5,如何构造生成0和1的概率分别为0.3、0.7的随机生成器?
法:对0、1进行组合。
1 int MyFun() 2 { 3 int n1=fun(); 4 int n2=fun(); 5 int n3=fun(); 6 int n4=fun(); 7 int n=n1; 8 n|=n2<<1; 9 n|=n3<<2; 10 n|=n4<<3; 11 if(n<=2) return 0; 12 else if(n<10) return 1; 13 else return MyFun(); 14 }
随机生成器生成0和1的概率分别为p和1-p,如何构造等概率随机生成0和1的生成器?
法:由于生成01和10的概率均为p(1-p),所以可以根据之实现:
1 int MyFun() 2 { 3 int n1=fun(); 4 int n2=fun(); 5 int n=n1(); 6 n|=n2<<1; 7 if(n==2) return 0; 8 else if(n==1) return 1; 9 else return MyFun(); 10 }
9、一个递减序列循环左移若干位后,从其中查找一个数的O(lgn)方法:类似于折半查找
1 int find(int data[],int n,int v) 2 { 3 int s=0,e=n-1,mid; 4 while(s<=e) 5 { 6 mid=s+(e-s)/2; 7 if(v==data[mid]) return mid; 8 else if( (data[s]>=v && v>data[mid]) || (v<=data[s] && data[s]<data[mid]) ||(v>data[mid] && data[s]<data[mid]) ) 9 {//有三种情况使得落于左半子序列:左半子序列递减且v位于其间、左半子序列非递减且v位于非递减的前段如21765、左半子序列非递减且v位于非递减的后段如21765、 10 e=mid-1; 11 } 12 else//落于右半子序列的情况类似 13 { 14 s=mid+1; 15 } 16 } 17 return -1; 18 }
其他(quiz)
蛇形打印(打印对角行元素,只不过每次右上、左下方向依次交换)方阵:
1 #include <stdio.h> 2 3 #define M 100 4 5 void traverse(int a[][M],int n) 6 {//每次打印的数据个数为1、2、3、...、n、n-1、...、2、1,依次打印之,只不过右上方向和左下方向时坐标相应地增减下 7 int i=0,j=0; 8 int count,loop; 9 int dir=1; 10 for(count=1;count<=n;count++) 11 { 12 if(dir==1) 13 {//右上方向 14 for(loop=1;loop<count;loop++) 15 { 16 printf("%d ",a[i--][j++]); 17 } 18 printf("%d ",a[i][j]); 19 if(j==n-1) i++; 20 else j++; 21 } 22 else 23 {//左下方向 24 for(loop=1;loop<count;loop++) 25 { 26 printf("%d ",a[i++][j--]); 27 } 28 printf("%d ",a[i][j]); 29 if(i==n-1) j++; 30 else i++; 31 } 32 dir=-dir; 33 } 34 35 for(count=n-1;count>0;count--) 36 { 37 if(dir==1) 38 { 39 for(loop=1;loop<count;loop++) 40 { 41 printf("%d ",a[i--][j++]); 42 } 43 printf("%d ",a[i][j]); 44 if(j==n-1) i++; 45 else j++; 46 } 47 else 48 { 49 for(loop=1;loop<count;loop++) 50 { 51 printf("%d ",a[i++][j--]); 52 } 53 printf("%d ",a[i][j]); 54 if(i==n-1) j++; 55 else i++; 56 } 57 dir=-dir; 58 } 59 } 60 61 int main(int argc,char *argv[]) 62 { 63 int n=4; 64 int a[n][M]; 65 for(int i=0;i<n;i++) 66 { 67 for(int j=0;j<n;j++) 68 { 69 a[i][j]=n*i+j+1; 70 } 71 } 72 traverse(a,n); 73 }