给定一个整数n以及一个二维整数序列arrarr中的数值介于[0,n)之间,现要求从arr中挑选出部分子序列,使得这些子序列的并集范围囊括[0,n),问满足条件的最小子序列数量为几?

举个例子:
1
2
3
4
5
6
7
8
9
arr = [[0, 2],
[1, 2],
[0, 1],
[0]]
显然满足条件的最小子序列数量为2,这些可能的子序列组合为(有四种情况):
1. [0, 2] + [1, 2] = [0, 1, 2]
2. [0, 2] + [0, 1] = [0, 1, 2]
3. [1, 2] + [0, 1] = [0, 1, 2]
4. [1, 2] + [0] = [0, 1, 2]

遍历所有的子序列的组合,找出其中所有满足条件的子序列,最后输出其中最小数量即可。显然可以采用递归来进行深度遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
n=3
candidates = [[0, 2],
[1, 2],
[0, 1],
[0]]

all_groups=[] #存放所有满足条件的子序列组合
def traverse(arr,depth):
if depth==0:
if set(flatten(arr))>=set(range(n)): #flatten()函数参见:
#https://github.com/muggledy/myworld/blob/master/src/_base/funcs.py
all_groups.append(arr)
return
traverse([*arr,candidates[-depth]],depth-1)
traverse([*arr],depth-1)

traverse([],len(candidates))
from pprint import pprint
pprint(all_groups)

'''OUTPUT
[[[0, 2], [1, 2], [0, 1], [0]],
[[0, 2], [1, 2], [0, 1]],
[[0, 2], [1, 2], [0]],
[[0, 2], [1, 2]],
[[0, 2], [0, 1], [0]],
[[0, 2], [0, 1]],
[[1, 2], [0, 1], [0]],
[[1, 2], [0, 1]],
[[1, 2], [0]]]
'''

问题需要的只是最小组合中的子序列数量,稍稍改一下上述代码:

1
2
3
4
5
6
7
8
9
10
11
all_groups_num=[]
def traverse(arr,depth,devote): #devote参数记录每一个满足条件的组合中的子序列数量
if depth==0:
if len(set(arr))>=n:
all_groups_num.append(devote)
return
traverse(arr+candidates[-depth],depth-1,devote+1)
traverse(arr,depth-1,devote)

traverse([],len(candidates),0)
print(min(all_groups_num)) #2

我的提交

sergfsm利用itertools.combinations(iterable,r)函数,其将创建一个迭代器,返回参数iterable中所有长度为r的子序列组合(譬如combinations('ABC',2)将返回[('A', 'B'), ('A', 'C'), ('B', 'C')]),可以说相当契合本题题意了:

1
2
3
4
5
from itertools import combinations
def perfect_team_of_minimal_size(n, candidates):
for j in range(1, len(candidates)+1):
if any(len(set(sum(i,[]))) >= n for i in combinations(candidates, j)): return j+1
return -1

从最小的组合数开始寻找,一旦符合条件,就停止寻找,而我的递归解法则需要穷尽所有组合

2021.6.14

给定一个二维数组arr,判断是否是二维回文数组(Sator Square)?

举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
                              v
arr = [['S', 'A', 'T', 'O', 'R'],
['A', 'R', 'E', 'P', 'O'],<
['T', 'E', 'N', 'E', 'T'],
> ['O', 'P', 'E', 'R', 'A'],
['R', 'O', 'T', 'A', 'S']]
^
如何判断其是否是二维回文的?那就是不论从哪个方向阅读都是同一字符串,
如上箭头所指,从下到上读(第二列):OPERA,从上到下读(倒数第二列):OPERA,
从右到左读(第二行):OPERA,从左到右读(倒数第二行):OPERA,
同理还有,从下到上读(第一列):ROTAS,从上到下读(倒数第一列):ROTAS,
从右到左读(第一行):ROTAS,从左到右读(倒数第一行):ROTAS,
同理还有,从下到上读(第三列):TENET,从上到下读(倒数第三列):TENET,
从右到左读(第三行):TENET,从左到右读(倒数第三行):TENET
1
2
3
4
5
6
7
8
9
import numpy as np

def judge(arr):
arr=np.array(arr)
return all([all([all(arr[:,i][::-1]==arr[:,-i-1]), \
all(arr[:,i][::-1]==arr[i][::-1]), \
all(arr[:,i][::-1]==arr[-i-1])]) for i in range(int(len(arr)/2+0.5))])

print(judge(arr)) #输出True或False

我的提交

参考RazNaot的解法,实际上,前面所说的判断规则可以进一步转变为:

  1. 二维数组旋转180度后还是自身(第i列等于倒数第i列的倒序,或者,第i行等于倒数第i行的倒序)
  2. 二维数组转置后还是自身(第i列等于第i行)
1
2
3
4
5
def judge(a):
b=list(zip(*a)) #转置
a=list(zip(*b)) #转置的转置还是自身,这一步的目的是使a、b、c都统一为元组列表,可以删除此语句看看a、b、c三者输出
c=[i[::-1] for i in a[::-1]] #旋转180度
return a==b==c

ZozoFouchtra提供了上述写法的简写:

1
2
def judge(a):
return a == [t[::-1] for t in a][::-1] == list(map(list, zip(*a))) #此处统一为列表之列表
2021.6.15

给定一个序列arr,将其中的奇数按升序排列,偶数保持不动

举个例子:
1
2
3
[7, 1]  =>  [1, 7]
[5, 8, 6, 3, 4] => [3, 8, 6, 5, 4]
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0] => [1, 8, 3, 6, 5, 4, 7, 2, 9, 0]
1
2
3
4
5
6
import numpy as np
a=[5, 8, 6, 3, 4]
a=np.array(a)
inds=np.where(a%2==1)[0]
a[inds]=np.sort(a[inds])
print(a) #[3 8 6 5 4]

我的提交

jgdodson的解法:

1
2
3
def sort_array(arr):
odds = sorted((x for x in arr if x%2 != 0), reverse=True)
return [x if x%2==0 else odds.pop() for x in arr]

MFreidank的解法(同上):

1
2
3
def sort_array(source_array):
odds = iter(sorted(v for v in source_array if v % 2))
return [next(odds) if i % 2 else i for i in source_array]
2021.6.15

给定一个二元组列表,其中每一个二元组(a,c)代表直线斜截式方程y=ax+cy=ax+c的两个参数a(斜率)和c(直线在y轴上的截距),要求所有直线的绝对值方程的和函数,一个分段函数

举个例子:
1
2
3
4
5
6
7
8
给定一个二元组列表[(0.25,-3),(-1,1)],也就是一组直线:
y = 0.25 x - 3
y = -x + 1
求这组直线的绝对值和函数,即|0.25x-3| + |-x+1|:
y = -1.25 x + 4 介于 [-∞,1] 区间
y = 0.75 x + 2 介于 [1,12] 区间
y = 1.25 x - 4 介于 [12,+∞] 区间
最终返回[(-∞,1,-1.25,4),(1,12,0.75,2),(12,+∞,1.25,-4)],形式为[(左界,右界,斜率,截距),...]
001010-10-101010

问题本身很简单,就是求分段函数嘛,首先找出所有的零点,也就是分段点,然后再分别求出每一段的(直线)函数,没什么好说的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
inf=float('inf')
lines=[(1.0, 3.0), (0, 2), (1.0, 5.0)] #lines=[(2, 2), (3, 3), (4, 1), (8, 2)]

base=0 #考虑到部分为水平直线,即形如y=0x+b,这部分剥出来,最后直接在分段函数上加个常数即可
dot0s=[]
for i,(a,c) in enumerate(lines):
if a==0:
base+=c
else:
dot0s.append(-c/a) #所有的零点

dot0s=sorted(set(dot0s)) #不同直线的零点可能重合,由于浮点数计算的不精确,明明是重合的零点,但是set操作却无法做到unique化

ret=[]
for left,right in zip([-inf,*dot0s],[*dot0s,inf]): #left,right是分段函数的左右界点
cd=((right-1 if left<0 and isinstance(left,type(inf)) else left)+(left+1 if right>0 and isinstance(right,type(inf)) else right))/2 #分段的中间点
_as,_cs=zip(*[(a,c) if a*cd+c>0 else (-a,-c) for a,c in lines if a!=0])
s_as,s_cs=sum(_as),sum(_cs,base)
ret.append((left,right,s_as,s_cs))

print(ret)

稍稍简化一下写法,并且使用decimal.Decimal做精确浮点数计算:

1
2
3
4
5
6
7
8
9
10
11
12
from decimal import Decimal
def expand(lines):
lines=[tuple(map(Decimal,line)) for line in lines]
inf,base,dot0s,ret=float('inf'),0,[],[]
[(base:=base+c) if a==0 else dot0s.append(-c/a) for i,(a,c) in enumerate(lines)] #这里使用了海象运算符,需要python3.8或以上版本
dot0s=sorted(set(dot0s))
for left,right in zip([-inf,*dot0s],[*dot0s,inf]):
_as,_cs=zip(*[(a,c) if a*(((right-1 if left<0 and isinstance(left,type(inf)) \
else left)+(left+1 if right>0 and isinstance(right,type(inf)) else right))/2)+ \
c>0 else (-a,-c) for a,c in lines if a!=0])
ret.append((left,right,sum(_as),sum(_cs,base)))
return [tuple(map(float,i)) for i in ret]

这也是一开始尝试提交的代码,测试过了,但是提交报错:Execution Timed Out。因此我又用numpy矩阵运算重写了上述代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from decimal import Decimal
import numpy as np
inf=float('inf')
def expand(lines):
lines=[tuple(map(Decimal,line)) for line in lines]
ndlines=np.array(lines) #形状为(n,2),n是直线数量
t=ndlines[np.where(ndlines[:,0]!=0)]
dot0s=np.sort(np.unique(-t[:,1]/t[:,0])) #全部零点(从左至右)

leftright=np.array([[dot0s[0]-1,*dot0s],[*dot0s,dot0s[-1]+1]]) #形状为(2,k),k是分段数量
mids=leftright.sum(axis=0)/2 #分段的中间点,形状为(k,)

mask=np.where((ndlines[:,[0]]*mids+ndlines[:,[1]])>0,1,-1) #掩码矩阵,取值为1或-1,形状为(n,k),任意行表示某直线在从左到右各个分段是否取负值(绝对值,负负得正)
leftright[[0,-1],[0,-1]]=Decimal(-inf),Decimal(inf)
return np.vstack((leftright,(np.transpose(np.broadcast_to(ndlines,(len(mids),*ndlines.shape)))*mask).sum(axis=1))).astype('float').T.tolist()

print(expand([(2, 2), (3, 3), (4, 1), (8, 2)])) #[[-inf, -1.0, -17.0, -8.0], [-1.0, -0.25, -7.0, 2.0], [-0.25, inf, 17.0, 8.0]]

结果仍然是Execution Timed Out。然后我将expand()函数的第一行代码注释掉,就ok了…
我的提交

Blind4Basics提供的解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def expand(arr, inf=float('inf')):
A,C,lst = 0,0,[]
for a,c in arr:
x0 = -c/a
a,c = abs(a), abs(c)
if x0<0: c*=-1
A-=a ; C+=c
if a: lst.append( (x0,a,-c) )

lst.sort()
x,out = -inf,[]
for x0,a,c in lst:
if x!=x0: out.append( (x,x0,A,C) )
x,A,C = x0, A+2*a, C+2*c
else:
out.append( (x,inf,A,C) )
return out

略,没看懂

2021.6.15

给定一个长度为4的倍数的正整数序列arr=[x1,x2,x3,x4,,xm,xm+1]arr = [x_1, x_2, x_3, x_4, \cdots, x_m, x_{m+1}],记P=(x12+x22)×(x32+x42)××(xm2+xm+12)P = (x_1^2 + x_2^2) \times (x_3^2 + x_4^2) \times \cdots \times (x_m^2 + x_{m+1}^2),要求满足条件P=A2+B2P=A^2+B^2的未知参数AABB(注:A,BA,B为非负整数,满足条件的AABB可能有多对,只要求出任意一对即可)

举个例子:
1
2
arr = [2, 1, 3, 4],于是P=125
满足条件的一对A、B分别为2、11

这不就是一个多元方程根求解的问题嘛,给定nnnn元方程,f1(x1,x2,...,xn)=0f_1(x_1,x_2,...,x_n)=0f2(x1,x2,...,xn)=0f_2(x_1,x_2,...,x_n)=0、…、fn(x1,x2,...,xn)=0f_n(x_1,x_2,...,x_n)=0,根据牛顿迭代法,将fi(x1,x2,...,xn)f_i(x_1,x_2,...,x_n)i=1,2,...,ni=1,2,...,n)在X0X_0(记X=[x1,x2,...,xn]TX=[x_1,x_2,...,x_n]^T)位置处做一阶泰勒展开并令其为0,得到:fi(X)=fi(X0)+fi(X0)T(XX0)=0f_i(X)=f_i(X_0)+\nabla f_i(X_0)^T(X-X_0)=0i=1,2,...,ni=1,2,...,n),于是有[f1(X0)f2(X0)fn(X0)]+[f1(X0)Tf2(X0)Tfn(X0)T](XX0)=[000]\begin{bmatrix}f_1(X_0)\\f_2(X_0)\\\vdots\\f_n(X_0)\end{bmatrix}+\begin{bmatrix}\nabla f_1(X_0)^T\\\nabla f_2(X_0)^T\\\vdots\\\nabla f_n(X_0)^T\end{bmatrix}(X-X_0)=\begin{bmatrix}0\\0\\\vdots\\0\end{bmatrix},可得迭代公式X:=X[f1(X)Tf2(X)Tfn(X)T]1[f1(X)f2(X)fn(X)]X:=X-\begin{bmatrix}\nabla f_1(X)^T\\\nabla f_2(X)^T\\\vdots\\\nabla f_n(X)^T\end{bmatrix}^{-1}\begin{bmatrix}f_1(X)\\f_2(X)\\\vdots\\f_n(X)\end{bmatrix}

牛顿迭代法回顾

牛顿法用于求解方程f(x)=0f(x)=0x=x0x=x_0附近的根(也称为切线法),设xx^*是方程f(x)=0f(x)=0的根,选取x0x_0作为其初始近似值,迭代的目的就是从x0x_0开始不断地去接近xx^*

具体的,过点(x0,f(x0))(x_0,f(x_0))作曲线y=f(x)y=f(x)的切线L0L_0,有L0L_0y=f(x0)(xx0)+f(x0)y=f^{'}(x_0)(x-x_0)+f(x_0)L0L_0与x轴交点的横坐标记为x1=x0f(x0)f(x0)x_1=x_0-\frac{f(x_0)}{f^{'}(x_0)},称x1x_1xx^*的一次近似值,再过点(x1,f(x1))(x_1,f(x_1))作曲线y=f(x)y=f(x)的切线L1L_1,求其与x轴交点的横坐标,并记为:x2=x1f(x1)f(x1)x_2=x_1-\frac{f(x_1)}{f^{'}(x_1)},称x2x_2xx^*的二次近似值,…,重复该过程得到xx^*的近似值序列{xn}\{x_n\},且有递推关系:xn+1=xnf(xn)f(xn)x_{n+1}=x_n-\frac{f(x_n)}{f^{'}(x_n)},称xn+1x_{n+1}是xx^*n+1n+1次近似值,当nn很大的时候,将得到一个无限接近根xx^*的近似精确解

看个例子,求解x3+2x2+3x+4=0x^3+2x^2+3x+4=0x=1x=1附近的根:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
#include<math.h>
#define True 1

double next(double x){
return x-(x*x*x+2*x*x+3*x+4)/(3*x*x+4*x+3);
}

int main(){
double eps=0.000001,x1=1,x2;
while(True){
x2=next(x1);
if(fabs(x1-x2)<eps) break;
x1=x2;
}
printf("%lf",x2); //-1.650629
return 0;
}

如上,主要是写出迭代关系式,用于求近似值序列{xn}\{x_n\}的下一项(见上述代码中的next()函数),另外为了控制迭代过程的结束,通常会设置一个精度值epseps,显然这个值越小,求解也越精确,有结束条件:xn+1xn<eps|x_{n+1}-x_n|<eps

牛顿迭代法的理论依据是什么?将f(x)f(x)在估计点x0x_0处展开为泰勒级数:f(x)=f(x0)+f(x0)(xx0)+f(x0)(xx0)22!++f(n)(x0)(xx0)nn!+Rn(x)f(x)=f\left(x_{0}\right)+f^{\prime}\left(x_{0}\right)\left(x-x_{0}\right)+\frac{f^{\prime \prime}\left(x_{0}\right)\left(x-x_{0}\right)^{2}}{2 !}+\cdots+\frac{f^{(n)}\left(x_{0}\right)\left(x-x_{0}\right)^{n}}{n !}+R_{n}(x),取其线性部分(泰勒展开的前两项),即在xx0x\rarr x_0时有f(x)=f(x0)+f(x0)(xx0)f(x)=f(x_0)+f'(x_0)(x-x_0)(无限接近)成立,此时要求f(x)=0f(x)=0的解,即求f(x0)+f(x0)(xx0)=0f(x_0)+f'(x_0)(x-x_0)=0的解,即得下一个估计点x=x0f(x0)f(x0)x=x_0-\frac{f(x_0)}{f'(x_0)},所以有迭代公式x:=xf(x)f(x)x:=x-\frac{f(x)}{f'(x)}

再看个例子,求平方根y=xy=\sqrt{x}xx是给定的常数值,我们还是改写一下形式吧,将xx记作aayy记作xx,形式变成x=ax=\sqrt{a},即问方程x2a=0x^2-a=0的根,立即写出迭代关系式:xn+1=xnxn2a2xnx_{n+1}=x_n-\frac{x_n^2-a}{2x_n},即:xn+1=xn2+a2xnx_{n+1}=\frac{x_n}{2}+\frac{a}{2x_n}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>
#include<math.h>
#define True 1

double sqrt1(double x){
//二分思想
//非常基本的解题思路,但性能比牛顿迭代差了好几倍
//需要注意的是,之所以可以采用二分思想,是因为y=x^(1/2)是单调函数,解区间不在左边就在右边
double start=0,end=x,mid,eps=0.000001; //start和end的初始值限定了解的范围,对于根号下x来说,设定解出于(0,x),但当x属于(0,1)时,就会出错,譬如sqrt(0.1)=0.316228,显然解非出自(0,0.1),也就是说我们的sqrt1函数不能处理(0,1)之间的小数
while(True){
mid=(start+end)/2;
//printf("start:%lf,end:%lf,mid:%lf\n",start,end,mid); //观察~
double t=mid*mid;
if(fabs(t-x)<eps) return mid;
else if(t>x){
end=mid;
}else if(t<x){
start=mid;
}
}
}

double sqrt2(double x){
//牛顿迭代
if(x==0) return 0;
double a=x,last,eps=0.000001;
do{
last=x; //我们将x本身作为初始的近似值
x=last/2+a/2/last;
//printf("%lf,%lf\n",last,x); //观察~
}while(fabs(x-last)>eps);
return x;
}

float InvSqrt(float x){
//平方根取倒数
//出自于Quake-III Arena的源代码,效率极高。基础原理仍是牛顿迭代,先给定一个初值,然后近似迭代,但是John Carmack(quake3的作者)NB的地方在于他选择了一个神秘的常数0x5f3759df来计算那个初始值,使得其只需要进行一次牛顿迭代就可以达到基本的精度要求!
//https://www.zhihu.com/question/30262900/answer/49589781
float xhalf = 0.5f*x;
int i = *(int*)&x; // get bits for floating value
i = 0x5f3759df - (i>>1); // gives initial guess y0
x = *(float*)&i; // convert bits back to float
x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy //要获取更高的精度,再重复一次(或更多)本行代码
//x = x*(1.5f-xhalf*x*x);
return x;
}

int main(){
//测试
for(int i=0;i<10;i++){
printf("[%d] sqrt1:%lf, math.sqrt:%lf, sqrt2:%lf\n",i,sqrt1(i),sqrt(i),sqrt2(i));
}

//printf("%f,%f",InvSqrt(5),1/sqrt(5));
return 0;
}

借助牛顿法求解函数的极小值,对于函数y=f(x)y=f(x),其极小值点xx^{*}满足f(x)=0f'(x^{*})=0(必要条件,反之不成立),只要找到这个xx^{*},那么极小值也就得到了(f(x)f(x^{*})),问题归结于求方程f(x)=0f'(x)=0的根,根据上面的结论,可以轻易得到迭代公式:x:=xf(x)f(x)x:=x-\frac{f'(x)}{f''(x)}。类似的,其理论依据仍可以由泰勒展开解释,现对f(x)f(x)在估计点x0x_0处作二阶泰勒展开,当xx0x\rarr x_0时有f(x)=f(x0)+f(x0)(xx0)+f(x0)(xx0)22!f(x)=f\left(x_{0}\right)+f^{\prime}\left(x_{0}\right)\left(x-x_{0}\right)+\frac{f^{\prime \prime}\left(x_{0}\right)\left(x-x_{0}\right)^{2}}{2!}(无限接近)成立,根据极小值点的必要条件,有f(x)=f(x0)+f(x0)(xx0)=0f'(x)=f'(x_0)+f''(x_0)(x-x_0)=0x=x0f(x0)f(x0)x=x_0-\frac{f'(x_0)}{f''(x_0)},所以有迭代公示x:=xf(x)f(x)x:=x-\frac{f'(x)}{f''(x)}

如果是多元函数,设y=f(x1,x2,...,xn)y=f(x_1,x_2,...,x_n),在估计点X0X_0处的二阶泰勒展开推广为:f(X)=f(X0)+f(X0)T(XX0)+(XX0)T2f(X0)(XX0)2f(X)=f(X_0)+\nabla f(X_0)^T(X-X_0)+\frac{(X-X_0)^{T}\nabla^{2} f(X_0)(X-X_0)}{2},我们用大写的XXX0X_{0}表示多维下的“点”,其由n个分量构成,形如X=[x1x2...xn]TX=[x_1 x_2 ... x_n]^{T}f\nabla fff的梯度(gradient)向量,f=[fx1fx2...fxn]T\nabla f=[\frac{\partial f}{\partial x_1} \frac{\partial f}{\partial x_2} ... \frac{\partial f}{\partial x_n}]^{T}2f\nabla^{2} fff的Hessian矩阵,其定义为:2f=[2fx122fx1x22fx1xn2fx2x12fx222fx2xn2fxnx12fxnx22fxn2]n×n\nabla^{2} f=\left[\begin{array}{cccc}{\frac{\partial^{2} f}{\partial x_{1}^{2}}} & {\frac{\partial^{2} f}{\partial x_{1} \partial x_{2}}} & {\cdots} & {\frac{\partial^{2} f}{\partial x_{1} \partial x_{n}}} \\ {\frac{\partial^{2} f}{\partial x_{2} \partial x_{1}}} & {\frac{\partial^{2} f}{\partial x_{2}^{2}}} & {\cdots} & {\frac{\partial^{2} f}{\partial x_{2} \partial x_{n}}} \\ \vdots & \vdots & {\ddots} & \vdots \\ {\frac{\partial^{2} f}{\partial x_{n} \partial x_{1}}} & {\frac{\partial^{2} f}{\partial x_{n} \partial x_{2}}} & {\cdots} & {\frac{\partial^{2} f}{\partial x_{n}^{2}}}\end{array}\right]_{n \times n},若简记f\nabla f2f\nabla^{2} fggHH,则分别表示在X0X_0点处所计算得到的实值梯度向量和实值Hessian矩阵f(X0)\nabla f(X_0)2f(X0)\nabla^{2} f(X_0)可简记为g(X0)g(X_0)H(X0)H(X_0),同样的,极小值点XX^*满足必要条件:f(X)=0\nabla f(X^*)=0,即在XX^*点处各个方向上的偏导数俱为0,对上述二阶泰勒展开式两边分别取\nabla并令其等于0,即有g(X0)+H(X0)(XX0)=0g(X_0)+H(X_0)(X-X_0)=0,若矩阵H(X0)H(X_0)可逆,有:X=X0H1(X0)g(X0)X=X_0-H^{-1}(X_0)g(X_0),于是有迭代公式X:=XH1(X)g(X)X:=X-H^{-1}(X)g(X)

看个简单的多元一次方程组的求解示例(其实多元一次方程组{a1ix1+a2ix2++anixn=bi}i=1,2,...,n\{a_1^ix_1+a_2^ix_2+\cdots+a_n^ix_n=b^i\}_{i=1,2,...,n}的求解根本用不着牛顿迭代,直接有[x1x2xn]=[a11a21an1a12a22an2a1na2nann]1[b1b2bn]\begin{bmatrix}x_1\\x_2\\\vdots\\x_n\end{bmatrix}=\begin{bmatrix}a_1^1&a_2^1&\cdots&a_n^1\\a_1^2&a_2^2&\cdots&a_n^2\\\vdots&\vdots&\ddots&\vdots\\a_1^n&a_2^n&\cdots&a_n^n\end{bmatrix}^{-1}\begin{bmatrix}b^1\\b^2\\\vdots\\b^n\end{bmatrix}):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import numpy as np
from itertools import count

eps=0.00001
max_iters=1000

def next(xn,params): #xn为迭代的近似方程根,形状为(n,1),params是一个(n,n)形的数组,任意行代表某个方程的系数部分,且包括最后的常数项(没有则置为0),形状应为(n,n+1)
return xn-np.linalg.pinv(params[:,:-1]).dot(params.dot(np.vstack([xn,np.array([[1]])])))

'''
给定方程组(示例来自http://www.jfc120.com/fcs):
12x + 7y = 1
10x + 5y + 2z = 1
9x + 5y + 2z = 1
于是params应设为:[[12,7,0,-1],[10,5,2,-1],[9,5,2,-1]]
代数解为(注意程序给出的有数值误差):
x=0
y=0.142857
z=0.142857
再譬如:
A+B+C+D=1
95A+94B+90C+84D=88
94A+85B+75C+55D=67
80A+75B+80C+69D=72
于是params应设为:[[1,1,1,1,-1],[95,94,90,84,-88],[94,85,75,55,-67],[80,75,80,69,-72]]
代数解为:
A=-0.033708
B=0.376404
C=0.101124
D=0.556180
'''

xn=np.array([1,1,1])[:,None] #随意给定的初始迭代值
params=[[12,7,0,-1],[10,5,2,-1],[9,5,2,-1]]

for i in count():
xn_plus1=next(xn,np.array(params,dtype='float'))
if np.sum(np.abs(xn_plus1-xn))<eps or i>max_iters:
if i>max_iters:
print('warning: max iterations!')
break
xn=xn_plus1

print(xn)

'''OUTPUT
[[-3.55271368e-15]
[ 1.42857143e-01]
[ 1.42857143e-01]]
'''

回到本题,虽然给的不是方程组,无法求逆,但可以用伪逆代替,然而由于只有一个方程,最终问题转变为了求满足A2+B2=PA^2+B^2=PA=BA=B的解,所得更是浮点数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import numpy as np
from itertools import count

eps=0.00001
max_iters=1000

def next(xn,p):
return xn-np.linalg.pinv(2*(xn.T)).dot((xn**2).sum(keepdims=True)-p)

xn=np.array([1,1])[:,None] #随意给定的初始迭代值

p=125 #13858000

for i in count():
xn_plus1=next(xn,p)
if np.sum(np.abs(xn_plus1-xn))<eps or i>max_iters:
if i>max_iters:
print('warning: max iterations!')
break
xn=xn_plus1

print(xn)

'''OUTPUT
[[7.9056955]
[7.9056955]]
'''

没办法,只好穷举实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np

def solve(arr):
p=np.cumprod((np.array(arr).reshape(2,-1,order='F')**2).sum(axis=0))[-1]
a=int(np.ceil(np.sqrt(p/2)))
b=int(np.ceil(np.sqrt(p)))
if b**2*2==p: #一旦满足立即返回
return [b,b]
x=np.arange(a,b+1)[:,None]
y=np.arange(1,a+1)[None,:]
inds=np.where((x**2+y**2)==p)
return [x.flat[inds[0]][0].item(),y.flat[inds[1]][0].item()] #如果要返回所有满足条件的结果,则return list(zip(x.flat[inds[0]],y.flat[inds[1]]))

print(solve([1, 3, 1, 2, 1, 5, 1, 9])) #[250, 210]

测试报错:MemoryError: Unable to allocate 1.25 PiB for an array with shape (8521545, 20572828) and data type int64,待分配的数组超出限制,修改:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np

def solve(arr):
p=np.cumprod((np.array(arr).reshape(-1,2)**2).sum(axis=1))[-1]
a=int(np.floor(np.sqrt(p/2)))
x=np.arange(1,a+1)
y=np.ceil(np.sqrt(p-x**2))
inds=np.where((x**2+y**2)==p)
return [x[inds[0]][0].item(),int(y[inds[0]][0].item())]

print(solve([3, 9, 8, 4, 6, 8, 7, 8, 4, 8, 5, 6, 6, 4, 4, 5])) #[7232, 13632]

测试通过,提交又出错:Execution Timed Out,我™透了,我蠢到这种地步了么

好像看不到答案…

2021.6.16

U(n,x)=x+2x2+3x3++nxnU(n, x) = x + 2x^2 + 3x^3 + \cdots + nx^n,且n+n\rightarrow +\infty,现给定s=U(n,x)s = U(n, x),问xx为多少(xx自然是在收敛域中,很明显,x<1x<1)?

举个例子:
1
2
给定 s = 2,则 x = 0.5
再譬如 s = 8,则 x = 0.7034648345913732

这是一个简单的数列求和,记S=x+2x2+3x3++nxnS=x + 2x^2 + 3x^3 + \cdots + nx^n,两边乘xxxS=x2+2x3+3x4++nxn+1xS=x^2+2x^3+3x^4+\cdots+nx^{n+1},前式减后式,有(1x)S=x+x2+x3++xnnxn+1(1-x)S=x+x^2+x^3+\cdots+x^n-nx^{n+1},由于x<1x<1nn无穷大时,nxn+1nx^{n+1}趋向于0,所以等号右边的数列和直接视为x+x2+x3++xnx+x^2+x^3+\cdots+x^n,再记T=x+x2+x3++xnT=x+x^2+x^3+\cdots+x^n,两边乘xxxT=x2+x3+x4++xn+1xT=x^2+x^3+x^4+\cdots+x^{n+1},前式减后式,有T=xxn+11xT=\frac{x-x^{n+1}}{1-x},当x<1x<1时,T=x1xT=\frac{x}{1-x},于是S=x(1x)2S=\frac{x}{(1-x)^2},将SS视为常数,问题变为求解一元二次方程Sx2(2S+1)x+S=0Sx^2-(2S+1)x+S=0的根,可根据之前介绍的牛顿迭代法计算,或者求根公式,此处采用牛顿法:

1
2
3
4
5
6
7
8
9
10
11
12
def solve(m):
eps=1e-12
next=lambda x:x-(m*x**2-(2*m+1)*x+m)/(2*m*x-2*m-1)
x=1
while 1:
xnplus1=next(x)
if abs(xnplus1-x)<eps:
break
x=xnplus1
return x

print(solve(8)) #0.7034648345913732

我的提交

Sherrbethead的求根公式解法:

1
2
def solve(m):
return (2*m + 1 - (4*m + 1)**0.5)/(2 * m)
2021.6.18

公司要给nn个人发奖金[s1,s2,...,sn][s_1,s_2,...,s_n],总金额为ss,为了保证公平,现依据每个人的缺勤天数[a1,a2,...,an][a_1,a_2,...,a_n]决定各自奖金的多寡,满足a1s1=a2s2==ansna_1s_1=a_2s_2=\cdots=a_ns_n,要求[s1,s2,...,sn][s_1,s_2,...,s_n]

举个例子:
1
2
3
譬如总金额s为$851,A、B、C三个人缺勤天数分别为18、15、12,公司给A、B、C各自发放的金额为$230、$276、$345,其满足所要求的两个条件:
1. 加起来等于总金额:$230 + $276 + $345 = $851
2. 18*$230 = 15*$276 = 12*$345

这个问题很简单,根据题意,不就相当于给出了nnnn元方程么:{s1+s2++sn=sa1s1a2s2=0a2s2a3s3=0an1sn1ansn=0\begin{cases}s_1+s_2+\cdots+s_n=s\\a_1s_1-a_2s_2=0\\a_2s_2-a_3s_3=0\\\,\,\,\,\,\,\,\,\,\,\,\,\,\,\vdots\\a_{n-1}s_{n-1}-a_ns_n=0\end{cases},接下来就不用多说了:

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np

def bonus(arr,s):
n=len(arr)
arr=np.array(np.broadcast_to(np.array(arr),(n,n)))
arr[0],arr[1:]=1,arr[1:]*(np.eye(n-1,n,k=0)-np.eye(n-1,n,k=1))
return np.around(np.linalg.inv(arr).dot(np.array([s if i==0 else 0 \
for i in range(n)])[:,None])).astype('int').flatten().tolist()

arr=[10, 11, 30, 12, 17, 23, 30, 11, 17, 10]
s=1788210
print(bonus(arr,s)) #[258060, 234600, 86020, 215050, 151800, 112200, 86020, 234600, 151800, 258060]

测试通过,提交出错:Execution Timed Out (12000 ms),我又透了,应该是通过其他方法求解?求逆太耗时?

2021.6.19

输入一个字符串,打印出该字符串中字符的所有排列。你可以以任意顺序返回这个字符串数组,但里面不能有重复元素

举个例子:
1
2
3
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
限制:1 <= s 的长度 <= 8

排列组合,老套路了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from typing import List

def permutation(s: str) -> List[str]:
ret=[]
def f(ss,t):
if len(ss)==1:
ret.append(t+ss)
return
for i,e in enumerate(ss):
f(ss[:i]+ss[i+1:],t+e)
f(s,'')
return list(set(ret))

print(permutation("abc")) #['bac', 'acb', 'bca', 'cba', 'cab', 'abc']

关于重复元素的去除,参考了Krahets的解题图释,代码更新如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from typing import List

def permutation(s: str) -> List[str]:
ret=[]
def f(ss,t):
if len(ss)==1:
ret.append(t+ss)
return
+ d=set()
for i,e in enumerate(ss):
+ if e in d:
+ continue
+ d.add(e)
f(ss[:i]+ss[i+1:],t+e)
f(s,'')
return list(set(ret))

print(permutation("abb")) #['bab', 'bba', 'abb']

我的提交

Krahets的解题思路(以下内容全部为摘抄):
对于一个长度为nn的字符串(假设字符互不重复),其排列方案数共有:n×(n1)×(n2)×2×1n×(n−1)×(n−2)…×2×1。根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符交换,先固定第11位字符(nn种情况)、再固定第22位字符(n1n-1种情况)、…、最后固定第nn位字符(11种情况)

重复排列方案与剪枝:
当字符串存在重复字符时,排列方案中也存在重复的排列方案。为排除重复方案,需在固定某位字符时,保证“每种字符只在此位固定一次”,即遇到重复字符时不交换,直接跳过。从DFS角度看,此操作称为“剪枝”

递归解析:

  1. 终止条件:当x = len(c) - 1时,代表所有位已固定(最后一位只有1种情况),则将当前组合c转化为字符串并加入res,并返回
  2. 递推参数:当前固定位x
  3. 递推工作:初始化一个集合set,用于排除重复的字符;将第x位字符与i ∈ [x, len(c)]字符分别交换,并进入下层递归,具体操作如下:
    1. 剪枝:若c[i]set中,代表其是重复字符,因此“剪枝”
    2. c[i]加入set,以便之后遇到重复字符时剪枝
    3. 固定字符:将字符c[i]c[x]交换,即固定c[i]为当前位字符
    4. 开启下层递归:调用dfs(x + 1),即开始固定第x + 1个字符
    5. 还原交换:将字符c[i]c[x]交换(还原之前的交换)

注:下图中list对应上文中的列表c

复杂度分析:

  1. 时间复杂度O(N!N)O(N!N)NN为字符串ss的长度;时间复杂度和字符串排列的方案数成线性关系,方案数为N×(N1)×(N2)×2×1N \times (N-1) \times (N-2) … \times 2 \times 1,即复杂度为O(N!)O(N!);字符串拼接操作join()使用O(N)O(N);因此总体时间复杂度为O(N!N)O(N!N)
  2. 空间复杂度O(N2)O(N^2):全排列的递归深度为NN,系统累计使用栈空间大小为O(N)O(N);递归中辅助set累计存储的字符数量最多为N+(N1)+...+2+1=(N+1)N/2N + (N-1) + ... + 2 + 1 = (N+1)N/2,即占用O(N2)O(N^2)的额外空间

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def permutation(s: str) -> List[str]:
c, res = list(s), []
def dfs(x):
if x == len(c) - 1:
res.append(''.join(c)) # 添加排列方案
return
dic = set()
for i in range(x, len(c)):
if c[i] in dic: continue # 重复,因此剪枝
dic.add(c[i])
c[i], c[x] = c[x], c[i] # 交换,将 c[i] 固定在第 x 位
dfs(x + 1) # 开启固定第 x + 1 位字符
c[i], c[x] = c[x], c[i] # 恢复交换
dfs(0)
return res
2021.6.22

给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案

举个例子:
1
2
3
4
5
6
7
8
9
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

输入:nums = [3,2,4], target = 6
输出:[1,2]

输入:nums = [3,3], target = 6
输出:[0,1]

好吧,我只会双重循环写,还是直接看其他人的解法吧
我的提交

官方给出的解决思路:
最容易想到的方法是枚举数组中的每一个数x,寻找数组中是否存在target - x。当我们使用遍历整个数组的方式寻找target - x时,需要注意到每一个位于x之前的元素都已经和x匹配过,因此不需要再进行匹配,所以我们只需要在x后面的元素中寻找target - x,时间复杂度为O(N2)O(N^2)。注意到该方法时间复杂度较高的原因是寻找target - x的时间复杂度过高,因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素,如果存在,我们需要找出它的索引。利用哈希表,可以将寻找target - x的时间复杂度从O(N)O(N)降低到O(1)O(1),这样我们创建一个哈希表,对于每一个x,我们首先查询哈希表中是否存在target - x,然后将x插入到哈希表中,即可保证不会让x和自己匹配。代码:

1
2
3
4
5
6
7
8
9
def twoSum(nums,target):
d={}
for i,e in enumerate(nums):
if target-e in d:
return [d[target-e],i]
d[e]=i
return [] #没找到返回空

print(twoSum([-3,4,3,90],87)) #[0, 3]
2021.6.23
正则表达式匹配(动态规划回顾)

给你一个字符串s和一个字符规律p,请你来实现一个支持'.''*'的正则表达式匹配,其中'.'匹配任意单个字符,'*'匹配零个或多个前面的那一个元素。所谓匹配,是要涵盖整个字符串s的,而不是部分字符串

举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
例1:
输入:s = "aa" p = "a"
输出:false
解释:"a" 无法匹配 "aa" 整个字符串
例2:
输入:s = "aa" p = "a*"
输出:true
解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次
例3:
输入:s = "ab" p = ".*"
输出:true
解释:".*" 表示可匹配零个或多个('*')任意字符('.')
例4:
输入:s = "aab" p = "c*a*b"
输出:true
解释:因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"
例5:
输入:s = "mississippi" p = "mis*is*p*."
输出:false

不会

查看官方给出的动态规划解法,此处略

动态规划回顾

看几个经典的案例:

  1. 找零问题

    设需要找零的总金额为 nn,且有 mm 种不同面值的硬币,记作 d=[d1,d2,...,dm]d=[d_1, d_2, ..., d_m],姑且假设有 d1<d2<...<dmd_1\lt d_2\lt...\lt d_m 成立,问至少需要多少枚硬币?
    假设 n=6n=6,并有两种面值,d=[d1,d2]=[1,2]d=[d_1,d_2]=[1,2],显然需要三枚面值为 2 的硬币,通常我们的出发点是,既然要换最少的硬币,那就首先挑选面值最大的,并且在现实中大家都是这么做的,贪心算法有其合理性,但并不能总是得到正确解,譬如当我们有三种面值 d=[d1,d2,d3]=[1,3,4]d=[d_1,d_2,d_3]=[1,3,4] 时,根据贪心算法,你会优先使用一枚面值为 4 以及两枚面值为 2 的硬币,但是“正确解”应该是两枚面值为 3 的硬币
    FiF_i 表示金额为 ii 时的最小硬币组合数,显然有 F6=min{F64+1,F63+1,F61+1}F_6=min\{F_{6-4}+1,F_{6-3}+1,F_{6-1}+1\},换句话说,你可以选择换一枚面值为 4 的硬币,剩下的两元钱怎么换你不用去管,这是一个更小的子问题,你可以交给另一个人去做,总之他会返回给你一个结果(硬币数),同理你还可以选择换一枚面值为 3 或者面值为 1 的硬币并将剩余金额的找零问题交给其他人去解,所以共有三个人接受你的委托,对于三个结果,选择最小的那个,再加上你的一枚硬币,就得到最少硬币数量了,更一般的,有{Fi=minj:idj,j1,2,...,m{Fidj+1}F0=0\begin{cases}F_i=\min\limits_{j:i\ge d_j,j\in{1,2,...,m}}\{F_{i-d_j}+1\} \\F_0=0\end{cases}(第一行为递归公式,第二行为边界条件)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #递归法解决找零问题

    n=6
    d=[1,3,4]

    def f1(i):
    '''返回最小硬币数'''
    if i==0:
    return 0
    return min([f1(i-j)+1 for j in d if i>=j])

    print(f1(n)) #试着将n取值200然后执行脚本,f1(n)非常慢(当前问题相当于在一棵三叉树中递归寻找最优解,当n=200时,树分支最深达到200,最浅的分支也达到50,假设深度全按50算,这意味着递归将发生3^50次函数调用),f2(n)则可以立即得到结果,但n若取值300,全部爆栈,建议改为循环

    from numpy import argmin
    from functools import lru_cache

    @lru_cache(maxsize=None)
    def f2(i):
    '''返回最小硬币数以及具体的找零方案'''
    if i==0:
    return 0,[]
    t=[(f2(i-j),j) for j in d if i>=j]
    m=argmin([j[0][0] for j in t])
    return t[m][0][0]+1,t[m][0][1]+[t[m][1]]

    print(f2(n))
    #print(f2.cache_info())
    循环解法

    譬如对于 d=[d1,d2,d3]=[1,3,4]d=[d_1,d_2,d_3]=[1,3,4],有:
    F1=min{F1d1+1}=F0+1=0+1=1F2=min{F2d1+1}=F1+1=1+1=2F3=min{F3d1+1,F3d2+1}=min{F2+1,F0+1}=min{3,1}=1F4=min{F4d1+1,F4d2+1,F4d3+1}=min{F3+1,F1+1,F0+1}=min{2,2,1}=1F5=min{F5d1+1,F5d2+1,F5d3+1}=min{F4+1,F2+1,F1+1}=min{2,3,2}=2F6=min{F6d1+1,F6d2+1,F6d3+1}=min{F5+1,F3+1,F2+1}=min{3,2,3}=2F_1=min\{F_{1-d_1}+1\}=F_0+1=0+1=1\\F_2=min\{F_{2-d_1}+1\}=F_1+1=1+1=2\\F_3=min\{F_{3-d_1}+1,F_{3-d_2}+1\}=min\{F_2+1,F_0+1\}=min\{3,1\}=1\\F_4=min\{F_{4-d_1}+1,F_{4-d_2}+1,F_{4-d_3}+1\}=min\{F_3+1,F_1+1,F_0+1\}=min\{2,2,1\}=1\\F_5=min\{F_{5-d_1}+1,F_{5-d_2}+1,F_{5-d_3}+1\}=min\{F_4+1,F_2+1,F_1+1\}=min\{2,3,2\}=2\\F_6=min\{F_{6-d_1}+1,F_{6-d_2}+1,F_{6-d_3}+1\}=min\{F_5+1,F_3+1,F_2+1\}=min\{3,2,3\}=2(其实这就是状态转移方程,通常动态规划你是画一个二维表然后逐行或者逐列填充,此处就是一维的表而已)
    由上述过程可以看出完全可以通过更简单的循环解决:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    n=6
    d=[1,3,4]

    F={0:0}

    for i in range(1,n+1):
    F[i]=min([F[i-j]+1 for j in d if j<=i])

    print(F[n]) #2

    正常的动态规划是怎么做的呢?上述递归解法我们将待找零的总金额给分解了,本来要找零100块钱,分解到最后你只需要找零1块钱,问题就变得非常简单,你还可以从另一个维度分解,即将可供找零的不同面值的钱币分解,本来有10种面值,分解到最后,只有一种面值,这时候找零问题就很简单了,下面画一个二维表,横轴是待找零的总金额,纵轴是可供找零的钱币面值:

    T[i][j]T[i][j] 为表中第 ii 行第 jj 列的元素(iji、j 均从 0 起始),表示当前要找零的金额为 s[j]s[j] 且可供找零的硬币面值为 d[0]d[i]d[0]到d[i] 时的最少硬币数量,姑且按行填充表格,第一行最简单,就不说了。看 T[1][3]T[1][3],当前要找零的金额为 3(s[3]s[3]),可以兑换一枚面值为 3 (d[1]d[1])的硬币,剩余 3-3=0 元继续找零,需要的最少硬币数为 T[1][0]T[1][0](这是已知的,因为表格是逐行填充的嘛),于是一共需要 1 + 0 枚硬币,另外我们已经知道,在“找零价钱为 3 且可供找零的硬币面值为 1”的情况下,最少硬币数为3(T[0][3]T[0][3] 的值),显然 1<3,于是最少硬币数量可以确定为 1。再看 T[1][6]T[1][6],当前要找零金额为 6,可以换一枚面值为 3 的硬币(为什么不兑换两枚面值为 3 的硬币呢?因为没必要,后面会说明),剩余 6-3=3 元继续找零,此前我们已经知道在“找零价钱为 3 且可供找零的硬币为 1、3”的情况下,最少硬币数为 1(也就是 T[1][3]T[1][3] 的值),于是一共需要 1+1=2 枚硬币,另外我们已经知道,在“找零金额为 6 且可供找零的硬币为 1”的情况下,最少硬币数为 6(T[0][6]T[0][6] 的值),而 2<6,于是最少硬币数量可以最终确定为 2。最后再看一下 T[2][6]T[2][6],当前要找零的金额为 6,可以换一枚面值为 4 的硬币,剩余 6-4=2 元继续找零,而 T[2][2]=2T[2][2]=2,于是一共需要使用 1+2=3 枚硬币,但是我们已经知道在“找零金额为 6 且可供找零的硬币为 1、3”的情况下,最少硬币数为 2(T[1][6]T[1][6]),3>2,因此最少数量仍保持之前的最优解,即 T[2][6]=T[1][6]=2T[2][6]=T[1][6]=2。一般的,有T[i][j]={T[i1][j],s[j]<d[i]min{T[i][s[j]d[i]]+1,T[i1][j]},otherwiseT[i][j]=\begin{cases}T[i-1][j],& s[j]\lt d[i] \\\min\{T[i][s[j]-d[i]]+1,T[i-1][j]\},& otherwise\end{cases}(状态转移方程)

    注:对于T[i][j]T[i][j],假设s[j]s[j]最多可以兑换KK枚面值为d[i]d[i]的硬币,为什么在上述求解过程中总是只兑换一枚呢?因为在状态转移过程中很显然总会有T[i][s[j]d[i]]+1=T[i][s[j]kd[i]]+kT[i][s[j]-d[i]]+1=T[i][s[j]-k·d[i]]+k1kK1\le k\le K)成立

  2. 装包问题
    现有四件物品,音响(3000 美元,重 4 磅)、笔记本电脑(2000 美元,重 3 磅)、吉他(1500 美元,重 1 磅)、iPhone(2000美元,重 1 磅),背包总容量为 4 磅,问背包最多能装入价值为多少的物品(注意每件物品的数量为 1,或者说一件物品最多只能装入一次)?注:该示例来自《图解算法》

    CELL[i][j]CELL[i][j] 为第 ii 行第 jj 列的单元格,含义为“在背包容量为jj且可供装入的物品为吉他、音响、笔记本电脑、iPhone中的前ii件时所能获取的最大价值”,其中 iji、j 均从 1 起始,此处以最后一个单元格为例进行说明,即T[4][4]T[4][4],此时iPhone的重量为 1 磅,背包可以容纳,假设装入,剩余 4-1=3 磅可供装入其它物品,根据 CELL[3][3]=2000CELL[3][3]=2000,即 3 磅容量可容纳的最大价值为 2000 美元(仅限于吉他、音响和电脑三件物品选择),于是“当前物品价值+剩余空间的价值”为 2000+2000=4000,而 4000>CELL[3][4]4000\gt CELL[3][4],其中 CELL[3][4]CELL[3][4] 表示在“背包容量为 4 磅,可供选择的物品为吉他、音响和电脑”的情况下最大价值为 3500,因此最大价值变为 4000 美元。其状态转移方程如下:

    或者用递归做:
    F(4,GSLI)=max{2000+F(3,GSL),2000+F(1,GSI),3000+F(0,GLI),1500+F(3,SLI)}F(3,GSL)=max{2000+F(0,GS),1500+F(2,SL)}F(1,GSI)=max{2000+F(0,GS),1500+F(0,SI)}F(0,GLI)=0F(3,SLI)=max{2000+F(2,SL),2000+F(0,SI)}F(2,SL)=max{什么也装不了}=0F(4,GSLI)=max\{2000+F(3,GSL),2000+F(1,GSI),3000+F(0,GLI),1500+F(3,SLI)\}\\F(3,GSL)=max\{2000+F(0,GS),1500+F(2,SL)\}\\F(1,GSI)=max\{2000+F(0,GS),1500+F(0,SI)\}\\F(0,GLI)=0\\F(3,SLI)=max\{2000+F(2,SL),2000+F(0,SI)\}\\F(2,SL)=max\{\text{什么也装不了}\}=0,其他诸如F(0,)F(0,·)也都为0
    于是最终 F(4,GSLI)=max{4000,4000,3000,3500}=4000F(4,GSLI)=max\{4000,4000,3000,3500\}=4000,对应的物品为 LILI,即笔记本电脑和iPhone。代码略

  3. 最长公共子序列(Longest Common Subsequence,LCS)
    假设给定两个字符串,要计算 其LCS的长度,这很简单,我只比较两个字符串的最后一个字符是否相同,如果相同,我就把两个字符串的最后一个字符都删除,然后将剩余部分交给另一个人,并问他同样的问题:“请告诉我这两个字符串的 LCS的长度”,他将返回给我一个值,我再加上 1 就是最终结果了。如果刚才最后一个字符比较不相同,那就删除第一个字符串的最后字符,第二个字符串保持不变,并交给一个人让他计算它们的 LCS的长度,另外再删除第二个字符串的最后字符,并保持第一个字符串不变,再交给另一个人让他计算它们的 LCS的长度,最终我会得到两个人返回的两个结果,选出最大的即可,问题解决,这是递归的思路,很容易得到相应的动态规划解法: 注. 图中箭头表示状态转移的方向

    其状态转移方程为:c[i,j]={0, if i=0 or j=0,c[i1,j1]+1, if i,j>0 and xi=yjmax(c[i,j1],c[i1,j]), if i,j>0 and xiyjc[i, j]=\begin{cases}0, & \text { if } i=0 \text { or } j=0, \\ c[i-1, j-1]+1, & \text { if } i, j>0 \text { and } x_{i}=y_{j} \\ \max (c[i, j-1], c[i-1, j]), & \text { if } i, j>0 \text { and } x_{i} \neq y_{j} \end{cases}。代码略

其他推荐:

  1. https://www.bilibili.com/video/BV1AB4y1w7eT
2021.6.27

给定一个字符串,找出其中不含有重复字符的最长子串的长度

举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3

输入: s = ""
输出: 0

只想到暴力求解(时间复杂度O(n2)O(n^2)):

1
2
3
4
5
6
7
8
def f(s):
for i in range(1,len(s)+1)[::-1]: #全部子串长度
for j in range(len(s)-i+1): #长度为i的全部子串
if len(set(s[j:j+i]))==len(s[j:j+i]):
return s[j:j+i]
return None

print(f('pwwkew')) #wke

提交出错,超出时间限制

画手大鹏提供了一种时间复杂度为O(n)O(n)的解法,是对官方解法的一个改进,官方时间复杂度其实是O(2n)O(2n),基本思路是:这个无重复字符的最长子串的起始位置无非就是[0,1,...,n1][0,1,...,n-1],对整个字符串进行一次遍历即可,由一个左指针控制,那么究竟是哪个呢,就看起始位置为ii时(即左指针指向位置ii时)所允许的最长无重复子串的长度是否大于其他任意起始位置处的最长无重复子串,右端的终止位置由一个右指针控制,假设其正指向位置i+ki+k,那么起始位置为i+1i+1的最长无重复子串的右端位置必然大于等于i+ki+k,以此类推,右指针也只需对整个字符串进行一次遍历

改进版图解:

2021.7.4

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 abc ,使得 a + b + c = 0 ?请找出所有和为 0 且不重复的三元组

举个例子:
1
2
3
4
5
6
7
8
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

输入:nums = []
输出:[]

输入:nums = [0]
输出:[]

遍历一次数组 nums 中的元素 e,将三数之和问题转换为两数之和(目标值为 0-e),而之前遇到过“两数之和”的问题,时间复杂度为 O(N),因此三数之和求解的时间复杂度为 O(N2)O(N^2)
我的提交

看下大佬Krahets的“排序+双指针”解法(以下内容为复制粘贴):

  • 双指针法铺垫:先将给定的数组 nums 排序,复杂度为 O(NlogN)O(NlogN)
  • 双指针法思路:固定 3 个指针中最左(最小)数字的指针 k,双指针 ij 分设在数组索引 (k, len(nums)) 两端,通过双指针交替向中间移动,记录对于每个固定指针 k 的所有满足 nums[k] + nums[i] + nums[j] == 0ij 组合:
    1. nums[k] > 0 时直接 break 跳出:因为 nums[j] >= nums[i] >= nums[k] > 0,即 3 个数字都大于 0 ,在此固定指针 k 之后不可能再找到结果了
    2. k > 0nums[k] == nums[k - 1] 时即跳过此元素 nums[k]:因为已经将 nums[k - 1] 的所有组合加入到结果(res)中,本次双指针搜索只会得到重复组合
    3. ij 分设在数组索引 (k, len(nums)) 两端,当 i < j 时循环计算 s = nums[k] + nums[i] + nums[j],并按照以下规则执行双指针移动:
      • s < 0 时,i += 1 并跳过所有重复的 nums[i]
      • s > 0 时,j -= 1 并跳过所有重复的 nums[j]
      • s == 0 时,记录组合 [k, i, j]res,执行 i += 1j -= 1 并跳过所有重复的 nums[i]nums[j],防止记录到重复组合
  • 复杂度分析:时间复杂度 O(N2)O(N^2),其中固定指针 k 循环复杂度 O(N),双指针 ij 复杂度 O(N)。空间复杂度 O(1),指针使用常数大小的额外空间
2021.7.5

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案

举个例子:
1
2
3
4
5
6
7
8
输入:nums = [1,2,0]
输出:3

输入:nums = [3,4,-1,1]
输出:2

输入:nums = [7,8,9,11,12]
输出:1

我的解法虽然时间复杂度为O(N),但是空间复杂度也是O(N),不太满足题意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from collections import defaultdict

nums=[3,4,-1,1]

d=defaultdict(bool)
m=None
for i in nums:
d[i]=True
if m is None:
m=i
else:
if i>m:
m=i
if m<0:
m=0
for i in range(1,m+2):
if d[i]==False:
print(i)
break

我的提交

看下官方提供的解题思路:
最基本的办法是,可以从1开始依次枚举正整数,并遍历数组nums,判断其是否在数组中,显然整个时间复杂度是O(N2)O(N^2),为了降低时间复杂度,可以将数组元素查找转化为字典或集合元素查找,时间复杂度从O(N)降低为O(1),仍然是从1开始依次枚举正整数,然后判断该正整数是否存在于哈希表中,于是整体时间复杂度变为O(N),这也正是我所采取的做法
实际上,对于一个长度为 NN 的数组,其中没有出现的最小正整数只能在 [1,N+1][1, N+1] 中,这是因为如果 [1,N][1, N] 都出现了,那么答案必然是 N+1N+1,否则答案将是 [1,N][1, N] 中没有出现的最小正整数。于是我们对数组 nums 进行遍历,对于遍历到的数 x,如果它在 [1,N][1, N] 的范围内,那么就将数组中的第 x-1 个位置(注意数组下标从 0 开始计数)打上「标记」,在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 N+1N+1,否则答案是最小的没有打上标记的位置加 1,如何打标记呢?你可以申请一个尺寸为 NN 的额外的标记数组,但是这违背了题目要求空间复杂度为 O(1) 的需求,因此官方提供了一个巧妙的思路,其将数组 nums 本身作为标记数组,以元素的正负作为「标记」,具体操作如下:

  1. 首先将数组 nums 中所有小于等于 0 的数修改为 N+1N+1
  2. 遍历数组 nums 中的每一个数 x,如果其介于 [1,N][1, N],则将 nums 位置下标为 x-1 的元素置为负数,如果已经是负数,则什么也不做
  3. 在遍历完成之后,如果数组 nums 中的每一个数都是负数,那么答案是 N+1N+1,否则答案是遇到的第一个正数的下标位置加 1
2021.7.6

给定一个非负整数数组 nums,你最初位于数组的第一个下标,数组中的每个元素代表你在该位置可以跳跃的最大长度,判断你是否能够到达最后一个下标

举个例子:
1
2
3
4
5
6
7
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标

题目做到这里,我已经一点信心都没有了。我摊牌了,我就是那个蠢蛋…

看了官方解答,让我想起本科安全编程的一道病毒传染问题,求最大传染范围,当时我使用了一个队列执行广度优先搜索(病毒一圈一圈地向外传染),首先针对传染源,获取其周围的邻居节点,将其添加到队列中,然后依次遍历队列中的元素,遍历过的元素将从队列头部弹出,继续搜索这些邻居节点的邻居节点,并依次添加到队列尾部,遍历直至队列为空
这个跳跃游戏和病毒感染非常类似,求最远可达位置,首先针对开始所处的位置也就是数组第一个下标(从0开始计数),其跳跃的最远可达位置为 0+nums[0],于是将所有可达位置 [1,...,nums[0]] (仅针对从未访问过的位置)添加到队列中,然后依次遍历队列中的元素,遍历过的元素将从队列头部弹出,继续搜索从这些可达位置可以跳跃到的所有可达位置,并依次添加到队列尾部,遍历直至队列为空,而队列中出现过的最大值就是待求,如果小于数组最后一个位置下标的数值,则游戏失败,否则成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nums = [3,2,1,0,4]

def f():
cur_pos=0 #当前位置
farthest_pos=0 #最远可达位置
n=len(nums)
while cur_pos<=farthest_pos and cur_pos<n:
farthest_pos=max([farthest_pos,cur_pos+nums[cur_pos]])
cur_pos+=1
if farthest_pos>=n-1:
print('成功')
else:
print('失败')

f() #失败

给定一个非负整数数组,你最初位于数组的第一个位置,数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置,假设你总是可以到达数组的最后一个位置

举个例子:
1
2
3
4
5
6
7
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
  从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置

输入: [2,3,0,1,4]
输出: 2
如果无脑用递归的话,那实在是太好写了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a = [3,2,0,1,4]

max_pos=len(a)-1 #数组最后一个位置下标

import math

def f(pos=0,count=0): #pos是当前位置,count是跳跃计数
if pos>=max_pos:
if pos==max_pos:
return count
return math.inf
if a[pos]==0: #如果当前位置的最远跳跃距离为0,表示无法到达数组末端
return math.inf
return min([f(pos+i,count+1) for i in range(1,a[pos]+1)])

print(f()) #2
由于递归深度限制以及时间限制,显然提交无法通过。官方采用了贪心算法,就不说了,可以看一下alchemist提供的动态规划解法,思路其实也很简单,但我等只能望洋兴叹了
2021.7.8

给定三个字符串 s1s2s3,请你帮忙验证 s3 是否是由 s1s2 交错组成的

举个例子:
1
2
3
4
5
6
7
8
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true

输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

输入:s1 = "", s2 = "", s3 = ""
输出:true

我只会递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
s1 = "aacbc"
s2 = "dbbcba"
s3 = "aadbbcbcabc"

def f(i=0,j=0,k=0): #判断s1[i:]、s2[j:]能否交错成s3[k:]
if len(s1[i:])+len(s2[j:])!=len(s3[k:]):
return False
if i>=len(s1) and j<len(s2):
return True if s2[j:]==s3[k:] else False
elif i<len(s1) and j>=len(s2):
return True if s1[i:]==s3[k:] else False
elif i>=len(s1) and j>=len(s2) and k>=len(s3):
return True
if s3[k]==s1[i]:
if f(i+1,j,k+1):
return True
if s3[k]==s2[j]:
if f(i,j+1,k+1):
return True
return False

print(f()) #True

不出所料提交超出时间限制…

看下官方提供的动态规划解法(以下为复制粘贴),哇,佩服,唉,为什么我就是不能学以致用呢?我终究仍未掌握那天所学的DP算法…
首先如果s1+s2s3|s_1|+|s_2|\ne |s_3|(此处|·|表示字符串的长度),那s3s_3必不可能由s1s_1s2s_2交错组成,在s1+s2=s3|s_1|+|s_2|= |s_3|时,可由动态规划求解,我们定义f(i,j)f(i, j)表示s1s_1的前ii个元素和s2s_2的前jj个元素是否能交错组成s3s_3的前i+ji+j个元素。如果s1s_1的第ii个元素(下标索引从0计数,即s1[i1]s_1[i-1])和s3s_3的第i+ji+j个元素(即s3[i+j1]s_3[i+j-1])相等,那么s1s_1的前ii个元素和s2s_2的前jj个元素是否能交错成s3s_3的前i+ji+j个元素取决于s1s_1的前i1i-1个元素和s2s_2的前jj个元素是否能交错组成s3s_3的前i+j1i+j-1个元素,即此时f(i,j)f(i,j)取决于f(i1,j)f(i-1,j),在此情况下如果f(i1,j)f(i-1,j)为真,则f(i,j)f(i,j)也为真。同样的,如果s2s_2的第jj个元素和s3s_3的第i+ji+j个元素相等并且f(i,j1)f(i,j-1)为真,则f(i,j)f(i,j)也为真。于是我们可以推导出这样的动态规划转移方程:f(i,j)=(f(i1,j)ands1[i1]==s3[p])or(f(i,j1)ands2[j1]==s3[p])f(i,j)=(f(i-1,j)\, and \,s_1[i-1]==s_3[p])\, or \,(f(i,j-1)\, and \,s_2[j-1]==s_3[p]),其中p=i+j1p=i+j-1,边界条件f(0,0)f(0,0)为真。真太妙了。代码略

2021.7.8

转战思源…


Sponsor