袋熊的树洞

日拱一卒,功不唐捐

0%

模型介绍

Softmax回归 (或者称多项Logistic回归) 是Logistic回归的一般化形式,这是多分类模型,二项Logistic回归是其特殊情况,因为类别只有两类。类似二项Logistic回归模型 (关于二项Logistic回归模型,可以参考我的文章) ,Softmax回归模型也是由条件概率 $P(Y | X)$ 表示,$Y$ 的取值不再是 ${0, 1}$ 两类,而是 ${1, 2, \ldots, K}$,其中 $K$ 是类别数,这里类别不从 $0$ 开始是为了后面公式推导方便,实际编码中,类别映射通常都从 $0$ 开始。我们定义不同类别对应的条件概率为:
$$
\begin{align}
P(Y = 1 | x) & = \frac{e^{\theta^T_1 x + b}}{\sum_{j=1}^K e^{\theta_j^T x + b}} \
P(Y = 2 | x) & = \frac{e^{\theta^T_2 x + b}}{\sum_{j=1}^K e^{\theta_j^T x + b}} \
\vdots \
P(Y = K | x) & = \frac{e^{\theta^T_K x + b}}{\sum_{j=1}^K e^{\theta_j^T x + b}}
\end{align}
$$
这里 $x \in R^{d}$ 是输入,$Y \in {1, 2, \ldots, K}$ 输出,$\theta_k\in R^{d} ; (k=1,\ldots, K)$ 和 $b \in R$ 是未知参数。 同二项Logistic模型那样,对于给定的输入 $x$,分别计算 $x$ 属于不同类别的概率,然后 $x$ 的类别分到概率值最大的那一类。有时为了方便,将权值向量 $\theta_k$ 和输入向量 $x$ 加以扩充,仍记作 $\theta_k$ 和 $x$,即 $\theta_k = [\theta_{k1}, \ldots, \theta_{kd}, b]^T \in R^{d+1}$,$x = [x_1, x_2, \ldots, x_d, 1] \in R^{d+1}$,此时条件概率为:
$$
P(Y = k | x) = \frac{e^{\theta^T_k x}}{\sum_{j=1}^K e^{\theta_j^T x}} \quad (k=1, 2, \ldots, K)
$$

参数估计

模型参数估计方法同二项Logistic回归模型那样,使用极大似然估计法 (MLE) 进行估计。已知输入的训练数据集 $T = { (x_1, y_1), (x_2, y_2), \ldots, (x_N, y_N) }$,其中 $x_i = [x_{i1}, \ldots, x_{id}, 1]^T \in R^{d+1}$,$y_i \in {1, 2, \ldots, K}$,待求的参数为 $\theta_k = [\theta_{k1}, \ldots, \theta_{kd}, b] \in R^{d+1} ; (k=1, 2, \ldots, K)$。前面不同类别的条件概率写成一条式子如下:
$$
P( Y = y | x) = \prod_{k=1}^K \left(\frac{e^{\theta^T_k x}}{\sum_{j=1}^K e^{\theta_j^T x}}\right)^{1{ y = k}}
$$
这里 $1{ \cdot }$ 为指indicator函数,定义为
$$
1{x } = \begin{cases}
1 & \text{x is a true statement} \
0 & \text{x is a false statement}
\end{cases}
$$
定义似然函数为:
$$
L(\theta_1, \ldots, \theta_K) = \prod_{i=1}^N \prod_{k=1}^K \left(\frac{e^{\theta^T_k x_i}}{\sum_{j=1}^K e^{\theta_j^T x_i}}\right)^{1{ y_i = k}}
$$
对数似然函数为:
$$
\ln(L(\theta_1, \ldots, \theta_K)) = \sum_{i=1}^N \sum_{k=1}^K 1{ y_i = k } \ln\left( \frac{e^{\theta^T_k x_i}}{\sum_{j=1}^K e^{\theta_j^T x_i}} \right)
$$
对 $\ln(L(\theta_1, \ldots, \theta_K))$ 求最大值,得到 $\theta_k$ 的估计值,在实现时,通常转换为求最小值,即对 $\ln(L(\theta_1, \ldots, \theta_K))$ 求最大值转换成对 $-\ln(L(\theta_1, \ldots, \theta_K))$ 求最小值:
$$
\min_{\theta_1, \ldots, \theta_K} -\sum_{i=1}^N \sum_{k=1}^K 1{ y_i = k } \ln \left( \frac{e^{\theta^T_k x_i}}{\sum_{j=1}^K e^{\theta_j^T x_i}} \right)
$$

模型实现

根据前面描述,模型参数估计主要是要求解下面这个优化问题:
$$
\min_{\theta_1, \ldots, \theta_K} -\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1{ y_i = k } \ln \left( \frac{e^{\theta^T_k x_i}}{\sum_{j=1}^K e^{\theta_j^T x_i}} \right)
$$
这里多乘一个系数 $\frac{1}{N}$ 并不会改变最优解的大小。上述问题可以用梯度下降法或者拟牛顿法进行求解,在求解过程中,需要计算目标函数的梯度,接下来推导梯度矩阵的表达式,这里首先令函数 $J(\theta_1, \ldots, \theta_K)$ 为损失函数 (目标函数):
$$
J(\theta_1, \ldots, \theta_K) = -\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1{ y_i = k } \ln \left( \frac{e^{\theta^T_k x_i}}{\sum_{j=1}^K e^{\theta_j^T x_i}} \right)
$$

梯度推导

为了表示方便,这里定义参数矩阵 $\theta$ 为
$$
\theta = \begin{bmatrix}
\theta_1 & \theta_2 & \cdots & \theta_K
\end{bmatrix} = \begin{bmatrix}
\theta_{11} & \theta_{21} & \cdots & \theta_{K1} \
\theta_{12} & \theta_{22} & \cdots & \theta_{K2} \
\vdots & \vdots & & \vdots \
\theta_{1d} & \theta_{2d} & \cdots & \theta_{Kd} \
\end{bmatrix}
$$
矩阵 $\theta$ 的大小为 $R^{d \times K}$,每一列为参数向量 $\theta_k , (k=1, \ldots, K)$,因此损失函数 $J(\theta_1, \ldots, \theta_K)$ 简写为 $J(\theta)$。为了后面推导方便,首先将损失函数分解为两部分:
$$
\begin{align}
J(\theta) & = -\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1{ y_i = k } \ln \left( \frac{e^{\theta^T_k x_i}}{\sum_{j=1}^K e^{\theta_j^T x_i}} \right) \
& = -\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \ln ( e^{\theta^T_k x_i} ) +\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \ln (\sum_{j=1}^K e^{\theta_j^T x_i}) \
& = -\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } (\theta^T_k x_i )+\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \ln (\sum_{j=1}^K e^{\theta_j^T x_i}) \
\end{align}
$$
令这两部分分别为 $f(\theta)$ 和 $g(\theta)$
$$
\begin{align}
f(\theta) &= -\frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } (\theta^T_k x_i ) \
g(\theta) & = \frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \ln (\sum_{j=1}^K e^{\theta_j^T x_i})
\end{align}
$$
损失函数 $J(\theta)$ 对矩阵 $\theta$ 某一个元素 $\theta_{pq}$ ($\theta_{pq}$ 代表参数向量 $\theta_q$ 的第 $p$ 个元素) 的偏导为:
$$
\frac{\partial{J(\theta)}}{\partial{\theta_{pq}}} = \frac{\partial{f(\theta)}}{\partial{\theta_{pq}}} + \frac{\partial{g(\theta)}}{\partial{\theta_{pq}}}
$$

第一部分

函数 $f(\theta)$ 对 $\theta_{pq}$ 偏导可以得到:
$$
\begin{align}
\frac{\partial{f(\theta)}}{\partial{\theta_{pq}}} & = -\frac{1}{N} \sum_{i=1}^N 1{ y_i = q } \frac{\partial{\theta_q^T x_i}}{\partial{\theta_{pq}}} \
& = -\frac{1}{N} \sum_{i=1}^N 1{ y_i = q } x_{ip}
\end{align}
$$

第二部分

函数 $g(\theta)$ 对 $\theta_{pq}$ 可以得到:
$$
\begin{align}
\frac{\partial{g(\theta)} }{\partial{\theta_{pq}}} & = \frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \frac{ \partial\ln(\sum_{j=1}^K e^{\theta_j^Tx_i}) }{ \partial{\theta_{pq}} } \
& = \frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \frac{1}{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \frac{\partial{e^{\theta_q^T x_i}} }{\partial{\theta_{pq}}} \
& = \frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \frac{ e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \frac{ \partial{\theta^T_q x_i} }{\partial{\theta_{pq}}} \
& = \frac{1}{N} \sum_{i=1}^N \sum_{k=1}^K 1 { y_i = k } \frac{ x_{ip} e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \
& = \frac{1}{N} \sum_{i=1}^N \frac{ x_{ip} e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \sum_{k=1}^K 1 { y_i = k }
\end{align}
$$
由于 $\sum_{k=1}^K 1 { y_i = k } = 1$,则最终可以得到
$$
\frac{\partial{g(\theta)} }{\partial{\theta_{pq}}} = \frac{1}{N} \sum_{i=1}^N \frac{ x_{ip} e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} }
$$

综合

综合第一部分和第二部分的推导,可以得到
$$
\begin{align}
\frac{\partial{J(\theta)}}{\partial{\theta_{pq}}} & = -\frac{1}{N} \sum_{i=1}^N 1{ y_i = q } x_{ip} + \frac{1}{N} \sum_{i=1}^N \frac{ x_{ip} e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \
& = -\frac{1}{N} \sum_{i=1}^N \left[ 1{ y_i = q } x_{ip} - \frac{ x_{ip} e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \right] \
& = -\frac{1}{N} \sum_{i=1}^N \left[ x_{ip} \left( 1{ y_i = q } - \frac{ e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \right) \right]
\end{align}
$$
如果要表示梯度向量 $\frac{\partial{J(\theta)}}{\partial{\theta_q}}$,可以写成如下形式:
$$
\frac{\partial{J(\theta)}}{\partial{\theta_q}} = -\frac{1}{N} \sum_{i=1}^N \left[ x_{i} \left( 1{ y_i = q } - \frac{ e^{\theta_q^T x_i} }{ \sum_{j=1}^K e^{\theta_j^Tx_i} } \right) \right]
$$

参考

  1. UFLDL Softmax Regression

二项Logistic回归模型

二项Logistic回归模型是一种分类模型,由条件概率分布 $P(Y|X)$ 表示,这里的随机变量取值为实数,随机变量 $Y$ 取值为 0 或 1,这个条件概率分布是未知的,需要通过监督学习方法来进行估计。条件概率分布如下表示:
$$
\begin{align}
P(Y=1 | x) & = \frac{e^{x^T \beta + b}}{1 + e^{x^T\beta + b}} = \frac{1}{1 + e^{-x^T \beta + b}} \
P(Y = 0 | x) & = \frac{1}{1 + e^{x^T \beta + b}}
\end{align}
$$
这里,$x\in R^d$ 是输入,$Y \in {0, 1}$ 是输出,$\beta \in R^d$ 和 $b$ 是参数。模型参数是未知的,需要进行模型训练获得,当获得模型参数时,对于一个新的输入实例 $x$,根据上面式子分别求出条件概率 $P(Y=1 | x)$ 和 $P(Y=0|x)$,比较两个条件概率值的大小,将实例 $x$ 分到较大的那一类。

模型参数估计

模型参数估计通常使用极大似然估计法 (MLE) 进行估计。已知输入的训练数据集 $T = {(x_1, y_1), \ldots, (x_N, y_N)}$,其中 $x_i = [x_{i1}, x_{i2}, \ldots, x_{id}]^T \in R^d$,$y_i \in {0, 1}$,待求的参数为 $\beta = [\beta_1, \beta_2, \ldots, \beta_d]^T \in R^d$ 和 $b \in R$。设:
$$
\begin{align}
P(Y=1|x) & = \phi(x) \
P(Y =0 | x) & = 1 - \phi(x)
\end{align}
$$
两个结合成一个式子可以写成如下表示:
$$
P(Y = y | x) = \phi(x)^{y} (1 - \phi(x))^{1- y}
$$
定义似然函数为:
$$
L(\beta, b) = \prod_{i=1}^N \phi(x_i)^{y_i} (1 - \phi(x_i))^{1-y_i}
$$

则对数似然函数为:
$$
\begin{align}
\ln (L(\beta, b)) & = \ln \left( \prod_{i=1}^N \phi(x_i)^{y_i} (1 - \phi(x_i))^{1-y_i} \right) \
& = \sum_{i=1}^N \left[ y_i \ln(\phi(x_i)) + (1- y_i) \ln(1-\phi(x_i))\right] \
& = \sum_{i=1}^N \left[ y_i \ln\left(\frac{\phi(x_i)}{1 - \phi(x_i)} \right) + \ln(1- \phi(x_i)) \right]
\end{align}
$$
根据前面模型描述,可以知道
$$
\phi(x) = \frac{e^{x^T \beta + b}}{1 + e^{x^T\beta + b}}
$$
则 $\frac{\phi(x)}{1-\phi(x)}$ 为
$$
\frac{\phi(x)}{1 - \phi(x)} = e^{x^T \beta + b}
$$
因此对数似然函数为:
$$
\begin{align}
\ln(L(\beta, b)) & = \sum_{i=1}^N \left[ y_i \ln(e^{x_i^T \beta}) + \ln\left( \frac{1}{1 + e^{x_i^T\beta + b}} \right) \right] \
& = \sum_{i=1}^N \left[ y_i (x_i^T \beta) - \ln(1 + e^{x_i^T\beta + b}) \right]
\end{align}
$$
对 $\ln(L(\beta, b))$ 求最大值,得到 $\beta$ 和 $b$ 的估计值,不过在实现时,通常转化为求解最小值,即对 $\ln(L(\beta, b))$ 求最大值转换成对 $-\ln(L(\beta, b))$ 求最小值:
$$
\min_{\beta, b} \sum_{i=1}^N \left[ \ln(1 + e^{x_i^T\beta + b}) - y_i (x_i^T \beta) \right]
$$

模型实现

根据前面描述,模型参数估计主要是要求解

$$
\min_{\beta, b} \frac{1}{N} \sum_{i=1}^N \left[ \ln(1 + e^{x_i^T\beta + b}) - y_i (x_i^T \beta) \right]
$$

这里多乘一个系数 $\frac{1}{N}$ 并不会改变最优解的大小。上述问题可以采用梯度下降法或者拟牛顿法进行求解,在求解过程中,需要计算目标函数的梯度。令函数 $f(\beta, b)$ 为:
$$
f(\beta, b) = \sum_{i=1}^N \left[ \ln(1 + e^{x_i^T\beta + b}) - y_i (x_i^T \beta) \right]
$$
则函数 $f(\beta, b)$ 对 $\beta_k ; (k=1, \ldots, d)$ 的偏导为
$$
\begin{align}
\frac{\partial{f(\beta, b)}}{\partial \beta_k} & = \frac{1}{N} \sum_{i=1}^N \left( \frac{x_{ik}e^{\beta^T x_i + b}}{1 + e^{\beta^T x_i + b}} -x_{ik} y_i \right) \
& = \frac{1}{N} \sum_{i=1}^N \left( \frac{x_{ik}}{1 + e^{-(\beta^T x_i + b)}} - x_{ik}y_i \right)
\end{align}
$$
令矩阵 $X \in R^{N \times d}$ 为
$$
X = \begin{bmatrix}
x_{11} & \ldots & x_{1d} \
x_{21} & \ldots & x_{2d} \
\vdots & & \vdots \
x_{N1} & \ldots & x_Nd
\end{bmatrix} = \begin{bmatrix}
x_1^T \
x_2^T \
\vdots \
x_N^T
\end{bmatrix}
$$
则 $X\beta + b$ 为
$$
X \beta + b = [\beta^T x_1 + b , \ldots, \beta^T x_N + b]^T
$$
令函数 $g(x)$ 为 Sigmoid 函数,也就是
$$
g(x) = \frac{1}{1 + e^{-x}}
$$
则 $g(X \beta + b)$ 为
$$
g(X \beta + b) = \begin{bmatrix}
\frac{1}{1 + e^{-(\beta^T x_1 +b)}} \
\frac{1}{1 + e^{-(\beta^T x_2 +b)}} \
\vdots \
\frac{1}{1 + e^{-(\beta^T x_N +b)}}
\end{bmatrix}
$$
令 $Xt =X^T$,矩阵 $Xt$ 第 $k$ 行为 $Xt[k, :]$,则可以得到
$$
\sum_{i=1}^N \frac{x_{ik}}{1 + e^{-(\beta^T x_i + b)}} = Xt[k, :] g(X\beta + b)
$$
以及
$$
\sum_{i=1}^N x_{ik} y_i = Xt[k, :] y
$$
因此
$$
\begin{align}
\frac{\partial{f(\beta, b)}}{\partial \beta_k} & = \frac{1}{N} \sum_{i=1}^N \left( \frac{x_{ik}e^{\beta^T x_i + b}}{1 + e^{\beta^T x_i + b}} -x_{ik} y_i \right) \
& = \frac{1}{N} \sum_{i=1}^N \left( \frac{x_{ik}}{1 + e^{-(\beta^T x_i + b)}} - x_{ik}y_i \right) \
& = \frac{1}{N} Xt[k, :] \left(g(X\beta + b) - y \right)
\end{align}
$$
则可得
$$
\begin{bmatrix}
\frac{\partial f(\beta, b)}{\partial \beta_1} \
\frac{\partial f(\beta, b)}{\partial \beta_2} \
\vdots \
\frac{\partial f(\beta, b)}{\partial \beta_d}
\end{bmatrix} = \frac{1}{N} \begin{bmatrix}
Xt[1, :] \left(g(X\beta + b) - y\right) \
Xt[2, :] \left(g(X\beta + b) - y\right) \
\vdots \
Xt[N, :] \left(g(X\beta + b) - y\right)
\end{bmatrix} = \frac{1}{N} X^T (g(X\beta+b)- y)
$$
前面是求函数 $f(\beta, b)$ 对 $\beta_k$ 的偏导,对于偏置项 $b$,函数 $f(\beta, b)$ 对 $b$ 的偏导为
$$
\begin{align}
\frac{\partial f(\beta, b)}{\partial b} & = \frac{1}{N} \sum_{i=1}^N \left( \frac{e^{\beta^T x_i + b}}{1 + e^{\beta^T x_i + b}} - y_i \right) \
& = \frac{1}{N} \sum_{i=1}^N \left( \frac{1}{1 + e^{-(\beta^T x_i + b)}} - y_i \right) \
& = \frac{1}{N} 1^T (g(X\beta + b) - y)
\end{align}
$$

这里的 $1$ 代表元素全是1的列向量。综上可以得到,函数 $f(\beta, b)$ 的梯度为:
$$
\nabla f(\beta, b) = \begin{bmatrix}
\frac{1}{N} 1^T (g(X\beta + b) - y) \
\frac{1}{N} X^T (g(X\beta+b)- y)
\end{bmatrix}
$$
这个梯度向量第一个元素代表 $\frac{\partial f(\beta, b)}{\partial b}$。知道梯度向量的表示,我们可以用梯度下降法或拟牛顿法 (例如BFGS等) 来求解优化问题,我这里采用BFGS算法来进行求解,核心代码如下:

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import numpy as np
from scipy.optimize import minimize


class LogisticRegression:
def __init__(self, alpha=1.0, max_iter=1000, tol=1e-4):
self.alpha = alpha
self.max_iter = max_iter
self.tol = tol

def sigmoid(self, x):
return 1 / (1.0 + np.exp(-x))

def objective(self, beta, X, y):
n, _ = X.shape
tmp = np.dot(X, beta[1:]) + beta[0]
object = np.sum(np.log(1+np.exp(tmp))) - np.dot(y, tmp)

return object / (1.0 * n)

def gradient(self, beta, X, y):
n, _ = X.shape
tmp = self.sigmoid(np.dot(X, beta[1:]) + beta[0]) - y
grads = []
grads.append(np.sum(tmp)/(1.0*n))
grads.extend(np.dot(X.T, tmp)/(1.0*n))

return np.array(grads)

def callback(self, beta):
self.history.append(self.objective(beta, self.X, self.y))

def fit(self, X, y):
n, p = X.shape
self.X = X
self.y = y
self.history = []

# initialize
beta0 = np.random.randn(p+1)

res = minimize(self.objective, beta0, args=(X, y), method='BFGS',
jac=self.gradient, callback=self.callback)

self.coef = res.x[1:]
self.intercept = res.x[0]

return self

def predict(self, X):
activation = self.sigmoid(np.dot(X, self.coef) + self.intercept)
y = np.zeros(X.shape[0], dtype=np.int)
y[activation >= 0.5] = 1

return y

def score(self, X, y):
y_predict = self.predict(X)

return np.mean(y == y_predict) * 100

@property
def coef_(self):
return self.coef

@property
def intercept_(self):
return self.intercept

@property
def history_(self):
return self.history

全部相关的代码在:GitHub

Coordinate Descent

在机器学习算法中,我们经常需要解一些优化问题,优化算法有很多,其中坐标下降法 (Coordinate Descent) 是一个很经典的算法,坐标下降法主要思想是每次迭代,只对一个坐标轴方向进行优化,其他坐标轴的值固定不变,使得多变量优化问题变成单变量优化问题。例如需要解决以下优化问题

$$
\min_{x} f(x_1, x_2, \cdots, x_d)
$$

其中 $x = (x_1, x_2, \cdots, x_d)^T \in R^d$。用坐标下降法解决该问题时,大概流程如下:

固定其他变量 $x_i (i\ne j)$ 不变,选择一个变量 $x_j$ 进行优化,然后更新该变量的值,接着选择下一个坐标轴方向进行优化,直到算法收敛。在算法中,选择坐标轴 $j$ 的方法有很多,可以随机选取,也可以对所有坐标轴循环一遍。

Lasso问题求解

Lasso优化问题通常如下表示:

$$
\min_{\beta} \frac{1}{2} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$

其中 $y \in R^{n}$,$X \in R^{n \times p}$,$\beta \in R^p$。令损失函数表示为 $f(\beta) = f(\beta_1, \cdots, \beta_p)$,即

$$
\begin{align}
f(\beta) &= \frac{1}{2} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1 \
& = \frac{1}{2} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)^2 + \lambda \sum_{j=1}^p \vert \beta_j \vert
\end{align}
$$

根据前面描述的坐标下降算法,要求第 $k$ 坐标轴方向的最优值,即求
$$
\min_{\beta_k} f(\beta_1, \cdots, \beta_{k-1}, \beta_k, \beta_{k+1}, \cdots, \beta_p)
$$
其中 $\beta_{j} {(j\ne k)}$ 已知且值固定不变。要求解该问题,可以通过梯度等于0来求解。对 $f(\beta)$ 求偏导可以得到:
$$
\begin{align}
\frac{\partial{f(\beta)}}{\partial{\beta_k}} & = -\sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& =- \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik} + \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = - r_k + z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k =\sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik}$,$z_k = \sum_{i=1}^n x_{ik}^2$,对于 $\frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}$,这里需要引入 subgradient 概念,具体内容可以参考这篇文章

$$
\frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} = \begin{cases}
\lambda & \beta_k \gt 0 \
[-\lambda, \lambda] & \beta_k = 0 \
-\lambda & \beta_k \lt 0
\end{cases}
$$

则可以得到 $f(\beta)$ 的偏导为:

$$
\frac{\partial{f(\beta)}}{\partial{\beta_k}} = \begin{cases}
-r_k + z_k \beta_k + \lambda & \beta_k \gt 0 \
[-r_k - \lambda, -r_k + \lambda] & \beta_k = 0 \
-r_k + z_k \beta_k - \lambda & \beta_k \lt 0
\end{cases}
$$

令 $\frac{\partial{f(\beta)}}{\partial{\beta_k}} = 0$,可以得到极小值点 $\beta_k^*$ ,由于 $\frac{\partial{f(\beta)}}{\partial{\beta_k}}$ 是个分段函数,需要分类讨论:

  • 当 $\beta_k \gt 0$ 时
    $$
    \begin{align}
    & -r_k + z_k \beta_k^* + \lambda = 0 \
    \Rightarrow \quad & \beta_k^* = \frac{r_k - \lambda}{z_k}
    \end{align}
    $$
    由于 $\beta_k \gt 0$,则 $\beta_k^* = \frac{r_k - \lambda}{z_k} \gt 0$,即 $r_k \gt \lambda$

  • 当 $\beta_k \lt 0$ 时
    $$
    \begin{align}
    & -r_k + z_k \beta_k^* - \lambda = 0 \
    \Rightarrow \quad & \beta_k^* =\frac{ r_k + \lambda}{z_k}
    \end{align}
    $$
    由于 $\beta_k \lt 0$,则 $\beta_k^* = r_k + \lambda \lt 0$,即 $r_k \lt -\lambda$

  • 当 $\beta_k = 0$ 时,可以得到 $0 \in [-r_k - \lambda, -r_k + \lambda]$,则可以得到
    $$
    \begin{cases}
    -r_k - \lambda \le 0 \
    -r_k + \lambda \ge 0
    \end{cases} \Rightarrow -\lambda \le r_k \le \lambda
    $$

综上所有内容,我们可以得到第 $k$ 个坐标轴方向的最优值为:
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$

定义一个函数 $S(a, \lambda)$,这个函数也称为 soft thresholding function
$$
S(a, \lambda) = \begin{cases}
a - \lambda & a \gt \lambda \
0 & -\lambda \le a \le \lambda \
a + \lambda & a \lt -\lambda
\end{cases}
$$
所以 $\beta_k^* = \frac{1}{z_k}S(r_k, \lambda)$,$k = 1, 2, \ldots, n$。这个函数也可以写成另一种形式
$$
S(a, \lambda) = (a-\lambda){+} - (-a - \lambda){+}
$$
这里 $(x)_{+} = \max(x, 0)$。

变种问题

由于Lasso优化问题的表示有很好几种,不同表示的优化问题,对应解的表示也会有所不同,虽然没什么大变化,但是还是罗列一下。

变种一

Lasso优化问题表示如下:
$$
\min_{\beta} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
令 $f(\beta) = \Vert y - X \beta \Vert_2^2 + \lambda \Vert \beta \Vert_1$,则 $\frac{\partial{f(\beta)}}{\partial{\beta_k}}$ 为:
$$
\begin{align}
\frac{\partial{f(\beta)}}{\partial{\beta_k}} & = -2 \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& =- 2 \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik} + 2 \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = - 2 r_k + 2z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k = \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik}$。使用 subgradient 可以进一步得到
$$
\frac{\partial{f(\beta)}}{\partial{\beta_k}} = \begin{cases}

  • 2r_k + 2 z_k \beta_k + \lambda & \beta_k \gt 0 \
    [-2r_k - \lambda, - 2 r_k + \lambda] & \beta_k = 0 \
  • 2r_k + 2 z_k \beta_k - \lambda & \beta_k \lt 0
    \end{cases}
    $$

根据前面讨论,令偏导为0,求得极值点 $\beta_k^*$ 为

$$
\beta_k^* = \begin{cases}
\frac{2r_k - \lambda}{2z_k} & 2r_k \gt \lambda \
0 & -\lambda \le 2r_k \le \lambda \
\frac{2r_k + \lambda}{2z_k} & 2r_k \lt -\lambda
\end{cases}
$$

用 soft thresholding function 表示为 $\beta_k^* = \frac{1}{2z_k} S(2r_k, \lambda)$

变种二

Lasso优化问题表示如下:
$$
\min_{\beta} \Vert X\beta - y\Vert^2_2 + \lambda \Vert \beta \Vert_1
$$

令 $f(\beta) = \Vert X \beta - y \Vert^2_2 + \lambda \Vert \beta \Vert_1$,则 $\frac{\partial{f(\beta)}}{\partial{\beta_k}}$ 为:
$$
\begin{align}
\frac{\partial{f(\beta)}}{\partial{\beta_k}} & = 2 \sum_{i=1}^n (\sum_{j=1}^p x_{ij} \beta_j - y_i) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = 2 \sum_{i=1}^n (\sum_{j \ne k} x_{ij} \beta_j - y_i)x_{ik} + 2 \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = 2 r_k + 2z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k = \sum_{i=1}^n (\sum_{j \ne k} x_{ij} \beta_j - y_i)x_{ik}$。使用 subgradient 可以进一步得到
$$
\frac{\partial{f(\beta)}}{\partial{\beta_k}} = \begin{cases}
2r_k + 2 z_k \beta_k + \lambda & \beta_k \gt 0 \
[2r_k - \lambda, 2 r_k + \lambda] & \beta_k = 0 \
2r_k + 2 z_k \beta_k - \lambda & \beta_k \lt 0
\end{cases}
$$
根据前面讨论,令偏导为0,求得极值点 $\beta_k^*$ 为
$$
\beta_k^* = \begin{cases}
\frac{-2r_k - \lambda}{2z_k} & -2r_k \gt \lambda \
0 & -\lambda \le -2r_k \le \lambda \
\frac{-2r_k + \lambda}{2z_k} & -2r_k \lt -\lambda
\end{cases}
$$

用 soft thresholding function 表示为 $\beta_k^* = \frac{1}{2z_k} S(-2r_k, \lambda)$

变种三

Lasso优化问题表示如下:
$$
\min_{\beta} \frac{1}{2 n} \Vert y - X\beta \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
同前面的推导,这里直接给出结果
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$
其中 $r_k =\frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j)x_{ik}$,$z_k = \frac{1}{n} \sum_{i=1}^n x_{ik}^2$

偏置 bias 求解

前面讨论的模型不包括偏置,如果模型包括偏置 $b$,则优化问题写为
$$
\min_{\beta, b} \frac{1}{2} \Vert y - X\beta - 1b \Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
这里的 $1$ 代表元素全为 1 的向量,大小为 $R^n$,损失函数 $f(\beta, b)$ 为
$$
\begin{align}
f(\beta, b) &= \frac{1}{2} \Vert y - X\beta - 1 b\Vert^2_2 + \lambda \Vert \beta \Vert_1 \
& = \frac{1}{2} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j - b)^2 + \lambda \sum_{j=1}^p \vert \beta_j \vert
\end{align}
$$
则对 $f(\beta, b)$ 求偏导 $\beta_k$ 可以得到
$$
\begin{align}
\frac{\partial{f(\beta, b)}}{\partial{\beta_k}} & = -\sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j - b) x_{ik} + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& =- \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j - b)x_{ik} + \sum_{i=1}^n x_{ik}^2 \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}} \
& = - r_k + z_k \beta_k + \frac{\partial{(\lambda \vert \beta_k \vert)}}{\partial{\beta_k}}
\end{align}
$$
其中 $r_k =\sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j -b)x_{ik}$,$z_k = \sum_{i=1}^n x_{ik}^2$,令 $\frac{\partial{f(\beta, b)}}{\partial{\beta_k}} = 0$,可以得到
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$
对 $f(\beta, b)$ 求偏导 $b$ 可以得到
$$
\begin{align}
\frac{\partial{f(\beta, b)}}{\partial{b}} & = -\sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j - b) \
& = n b - \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)
\end{align}
$$
令 $\frac{\partial{f(\beta, b)}}{\partial{b}} = 0$,可以得到
$$
b^* = \frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)
$$

算法实现

在本节,要解决的优化问题如下表示

$$
\min_{\beta, b} \frac{1}{2 n} \Vert y - X\beta - 1b\Vert^2_2 + \lambda \Vert \beta \Vert_1
$$
根据前面的讨论,在选定第 $k$ 坐标轴方向,解 $\beta^*_k$的表达式为
$$
\beta_k^* = \begin{cases}
\frac{r_k - \lambda}{z_k} & r_k \gt \lambda \
0 & -\lambda \le r_k \le \lambda \
\frac{r_k + \lambda}{z_k} & r_k \lt -\lambda
\end{cases}
$$

其中 $r_k =\frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j \ne k} x_{ij} \beta_j - b)x_{ik}$,$z_k = \frac{1}{n} \sum_{i=1}^n x_{ik}^2$,解 $b^*$ 的表达式为
$$
b^* = \frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j=1}^p x_{ij} \beta_j)
$$

这里可以将 $r_k$ 写成向量化形式,由于算法每次只更新一个坐标轴方向的值,即

$$
\begin{align}
& [\beta_1, \ldots, \beta_{k-1}, \beta_{k}, \beta_{k+1}, \ldots, \beta_p] \
\Rightarrow ; & [\beta_1, \ldots, \beta_{k-1}, \beta_{k}^*, \beta_{k+1}, \ldots, \beta_p]
\end{align}
$$
因此 $r_k$ 可以写为
$$
\begin{align}
r_k & =\frac{1}{n} \sum_{i=1}^n (y_i - \sum_{j =1}^p x_{ij} \beta_j - b + x_{ik} \beta_k)x_{ik}
\end{align}
$$
这里的 $\beta_k$ 是未更新之前的值,更新后的值为 $\beta_k^*$,注意区别。用向量表示 $r_k$ 为
$$
r_k = \frac{1}{n} (X[:, k])^T (y - X\beta - 1 b + \beta_k X[:, k])
$$

其中 $X[:, k]$ 代表矩阵 $X$ 第 $k$ 列。$b^*$ 的表达式写成向量形式可以写为
$$
b^* = \frac{1}{n} 1^T (y - X\beta)
$$
算法整体流程如下表示

核心代码如下

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class Lasso:
"""
The optimization objective of Lasso is
1/(2 * n) * ||y - Xw||^2_2 + alpha * ||w||_1
"""

def __init__(self, alpha=1.0, fit_intercept=True, normalize=False,
copy_X=True, max_iter=1000, tol=0.0001, selection='cyclic'):
self._alpha = alpha
self._fit_intercept = fit_intercept
self._normalize = normalize
self._copy_X = copy_X
self._max_iter = max_iter
self._tol = tol
self._selection = selection

if normalize:
self._scaler = preprocessing.StandardScaler()

def compute_step(self, k, X, y, coef, intercept, alpha):
n, p = X.shape
y_predict = np.dot(X, coef) + intercept
rk = np.dot(X[:, k], y - y_predict + X[:, k] * coef[k])
rk = rk / (1.0 * n)
zk = np.linalg.norm(X[:, k], ord=2) ** 2
zk = zk / (1.0 * n)
coef_k = (np.amax([rk-alpha, 0]) - np.amax([-rk-alpha, 0]))
coef_k = coef_k / (1.0 * zk)

return coef_k

def objective(self, X, y, coef, intercept, alpha):
n, p = X.shape
total = 0

y_predict = np.dot(X, coef) + intercept
total += \
1/(2.0*n) * np.linalg.norm(y-y_predict, ord=2) ** 2
total += alpha * np.linalg.norm(coef, ord=1)

return total

def fit(self, X, y):
if self._copy_X:
X = X.copy()
if self._normalize:
X = self._scaler.fit_transform(X)
self._objectives = []

# initialize data
num_samples, num_features = X.shape
coef = np.zeros(num_features)
old_coef = np.zeros(num_features)
intercept = 0
if self._fit_intercept:
tmp = y - np.dot(X, coef)
intercept = np.sum(tmp) / (1.0 * num_samples)
num_iters = 0
for iter in range(self._max_iter):
num_iters = num_iters + 1
if (self._selection == 'cyclic'):
for k in range(num_features):
old_coef[k] = coef[k]
coef[k] = self.compute_step(k, X, y, coef,
intercept, self._alpha)
if self._fit_intercept:
tmp = y - np.dot(X, coef)
intercept = np.sum(tmp) / (1.0 * num_samples)

# check condition of convergence
coef_updates = np.abs(coef - old_coef)
if np.amax(coef_updates) < self._tol:
break
self._objectives.append(self.objective(X, y, coef,
intercept, self._alpha))

self._coef = coef
self._intercept = intercept
self._num_iters = num_iters

return self

def predict(self, X):
if self._copy_X:
X = X.copy()
if self._normalize:
X = self._scaler.transform(X)

y_predict = np.dot(X, self._coef) + self._intercept

return y_predict

def score(self, X, y):
y_predict = self.predict(X)

return r2_score(y, y_predict)

@property
def coef_(self):
return self._coef

@property
def intercept_(self):
return self._intercept

@property
def n_iter_(self):
return self._num_iters

@property
def objectives_(self):
return self._objectives

def __str__(self):
return ('Lasso(alpha={}, copy_X={}, '
'fit_intercept={}, max_iter={}, '
'normalize={}, selection=\'{}\', '
'tol={})').format(self._alpha, self._copy_X,
self._fit_intercept, self._max_iter,
self._normalize, self._selection,
self._tol)

相关代码在:GitHub

参考

  1. A COORDINATE DESCENT ALGORITHM FOR THE LASSO PROBLEM
  2. SOFT-THRESHOLDING OPERATOR AND THE LASSO SOLUTION

题目一 找到差值为k的数字对的个数

问题描述

在n个元素的数组中,找到差值为k的数字对去重后的个数

输入描述:

1
2
第一行,n和k,n表示数字个数,k表示差值
第二行,n个正整数

输出描述:

1
差值为k的数字对去重后的个数

示例 1

输入

1
2
5 2
1 5 3 4 2

输出

1
3

说明

1
其中数字对分别为 (1, 3), (5, 3), (4, 2)

示例 2

输入

1
2
6 2
1 5 3 3 4 2

输出

1
3

说明

1
其中数字对分别为 (1, 3), (5, 3), (4, 2)

示例 3

输入

1
2
4 0
1 1 1 1

输出

1
1

说明

1
其中数字对分别为 (1, 1)

备注:

1
2
3
4
5
6
数据范围:
对于 100% 的数据,0 <= k < 100
对于 100% 的数据,数组内数值范围在 [0, 100000000]
对于 30% 的数据,0 < n < 1000
对于 80% 的数据,0 < n < 100000
对于 100% 的数据,0 < n < 1000000

解决方案

这道题目在 LeetCode 有相应的题目,题目为 532. K-diff Pairs in an Array,这道题目我在另一篇博文做了解答,可以参考 LeetCode 532 - K-diff Pairs in an Array

题目二 求函数调用次数

问题描述

定义两个字符串变量:s 和 m,再定义两种操作。

第一种操作:

1
2
m = s;
s = s + s;

第二种操作:

1
s = s + m;

假设 s, m 初始化如下:

1
2
s = "a";
m = s;

求最小的操作步骤数,可以将 s 拼接到长度等 n

输入描述:

1
2
一个整数n,表明我们需要得到s字符串长度
对于100%的数据,0 < n < 10000

输出描述:

1
一个整数,表明总共操作次数

示例 1

输入

1
6

输出

1
3

说明

1
输入是6,表明我们需要得到s字符串长度为6,也就是s最终为"aaaaaa",那么依次使用2次"第一次操作“和1次"第二种操作"就能达到目的,总共操作次数是3。

示例2

输入

1
5

输出

1
4

说明

1
假设n=5,表明我们需要得到s字符串长度为5,也就是s最终为"aaaaa",那么直接使用4次"第二种操作“就能达到目的,总共操作次数是4。

示例3

输入

1
4

输出

1
2

说明

1
假设n=4,表明我们需要得到s字符串长度为4,也就是s最终为"aaaa",那么直接使用2次"第一种操作"就能达到目的,总共操作次数是2。

题目三

没记录到。。。

题目四 Magic

问题描述

给一个包含n个整数元素的集合a,一个包含m个整数元素的集合b。定义magic操作为,从一个集合中取出一个元素,放到另一个集合里,且操作过后每个集合的平均值大于操作前。注意以下两点:

  1. 不可以把一个集合的元素取空,这样就没有平均值了
  2. 值为x的元素从集合b取出放入集合a,但集合a中已有值为x的元素,则a的平均值不变 (因为集合元素不会重复),b的平均值可能会改变 (因为x被取出了)

问最多可以进行多少次magic操作?

输入描述:

1
2
3
4
5
6
第一行为两个整数n,m
第二行n个整数,表示集合a中的元素
第三行m个整数,表示集合b中的元素
对于30%的数据,最终结果 <= 1
对于70%的数据,输入中的a,b集合元素完全没有重复,即 |a| + |b| = |a 并 b|
对于100%的数据,1 < n, m < 100,000,0 < a[i], b[i] < 100,000,000,集合a中元素互不相同,集合b中元素互不相同

输出描述:

1
输出一个整数,表示最多可以进行的操作次数。

示例 1

输入

1
2
3
3 5
1 2 5
2 3 4 5 6

输出

1
2

说明

1
依次从b集合取出3,4元素放入a集合

题目五 跳板小游戏

问题描述

小T最近迷上了一款跳板小游戏。已知空中有N个高度互不相同的跳板,小T刚开始在高度为0的地方,每次跳跃可以选择与自己当前高度绝对值差小于等于H的跳板,跳跃过后到达以跳板为轴的镜像位置,问小T在最多跳K次的情况下最高能跳多高? (任意时刻,高度不能为负)

输入描述:

1
2
第一行三个整数N,K,H
以下N行,每行一个整数Ti,表示第i个跳板的离地高度

输出描述:

1
一个整数,表示最高能跳到的高度

示例 1

输入

1
2
3
4
3 3 2
1
3
6

输出

1
8

说明

1
2
3
4
小T初始在高度为0的地方
第一次跳跃只能选择高度为1的跳板,结束后到达高度为2的地方,计算方式:高度1 = 初始高度 + (跳板1高度 - 初始高度)*2 = 0 + (1-0)*2 = 2
第二次跳跃选择高度为3的跳板,结束后到达高度为4的地方,计算方式:高度2 = 高度1 + (跳板2高度 - 高度1)*2 = 2 + (3-2)*2 = 4
第三次跳跃选择高度为6的跳板,结束后到达高度为8的地方,计算方式:高度3 = 高度2 + (跳板3高度 - 高度2)*2 = 4 + (6-4)*2 = 8

备注:

1
2
对于40%的数据,1 <= N, K, H, Ti <= 20
对于100%的数据,1 <= N, K, Ti <= 100,000, 1 <= H <= 100

问题描述

Given an array of integers and an integer k, you need to find the number of unique k-diff pairs in the array. Here a k-diff pair is defined as an integer pair (i, j), where i and j are both numbers in the array and their absolute difference is k.

Example 1

Input: [3, 1, 4, 1, 5], k = 2

Output: 2

Explanation: There are two 2-diff pairs in the array, (1, 3) and (3, 5).
Although we have two 1s in the input, we should only return the number of unique pairs.

Example 2

Input:[1, 2, 3, 4, 5], k = 1

Output: 4

Explanation: There are four 1-diff pairs in the array, (1, 2), (2, 3), (3, 4) and (4, 5).

Example 3

Input: [1, 3, 1, 5, 4], k = 0

Output: 1

Explanation: There is one 0-diff pair in the array, (1, 1).

Note:

  1. The pairs (i, j) and (j, i) count as the same pair.
  2. The length of the array won’t exceed 10,000.
  3. All the integers in the given input belong to the range: [-1e7, 1e7].

解决方案

这道题目主要意思是找出差值为 k 的数字对,而且数字对是不重复,例如数字对 (1, 3) 和数字对 (3, 1) 是重复的。首先判断数字 a 和数字 b 的差值是否为 k,对于数字 a 和数字 b 的差值,可以用 $\vert a - b \vert$表示,当差值为 k 时,即 $\vert a - b \vert = k$,去掉绝对值符号可以得到
$$
\begin{align}
& a = b - k \
\text{or} \quad & a = b + k
\end{align}
$$
也就说知道数字 a,可以利用上面等式判断一个数字 b 与数字 a 的差值是否为 k。接下来解决重复数字对问题,对于带有重复数字对序列 (1, 3), (3, 1), (3, 5), (5, 3),我们可以用一个无重复元素容器,例如STL中setmapunorder_setunorder_map,保存每一个数字对的最小元素,即 1, 3, 5,然后容器元素个数就是不重复数字对的个数。

综合上面的讨论,我们可以得到一个思路,首先创建两个容器,第一个容器保存访问过的元素,第二个容器为无重复元素容器,保存已找到的数字对的最小的元素,然后遍历数组的每一个元素,令当前访问的元素为 b,

  • 如果 $a = b-k$ 在第一个容器中,保存 $\min(a, b) = b - k$ 到第二个容器中
  • 如果 $a = b + k$ 在第一个容器中,保存 $\min(a, b) = b$ 到第二个容器中
  • 如果上述条件都不满足,说明没有找到相应的数字对,将元素 $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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;

class Solution {
public:
int findPairs(vector<int>& nums, int k) {
if (k < 0)
return 0;

int num;
unordered_set<int> pair_smalls;
unordered_set<int> unique_nums;
for (int i=0; i<nums.size(); i++) {
num = nums[i];
if (unique_nums.find(num - k) != unique_nums.end())
pair_smalls.insert(num - k);
if (unique_nums.find(num + k) != unique_nums.end())
pair_smalls.insert(num);

unique_nums.insert(num);
}

return pair_smalls.size();
}
};

int main()
{
Solution solu;

vector<int> nums1 = {3, 1, 4, 1, 5};
int k1 = 2;
cout << solu.findPairs(nums1, k1) << endl;

vector<int> nums2 = {1, 2, 3, 4, 5};
int k2 = -1;
cout << solu.findPairs(nums2, k2) << endl;

return 0;
}

题目一 在字符串中找出连续最长的数字串

问题描述

给定一个字符串,输出字符串中最长的数字串,并把这个数字串的长度输出。

请在一个字符串中找出连续最长的数字串,并把这个串的长度返回;如果存在长度相同的连续数字串,返回最后一个连续数字串。

__注意__:数字串只需要是数字组成的就可以,并不要求顺序,比如数字串”1234”的长度就小于数字串”1359055”,如果没有数字,就返回空字符串 (“”) 而不是NULL。

输入描述

1
一个字符串

输出描述

1
2
输出最长的数字串,输出最长数字串中数字个数
中间以逗号 (,) 隔开

示例 1

输入

1
abcd12345ed125ss123058789

输出

1
123058789,9

备注

  1. 如果存在长度相同的连续数字串,则输出最后一个连续数字串。
  2. 数字串只需要是数字组成的就可以,并不要求顺序,比如数字串”1234”的长度就小于数字串”1359055”。
  3. 如果没有数字,就返回空字符串 (“”) 而不是NULL。

解决方案

可以设定两对指针,每对指针包含字符串的起始位置和结束位置的后一个位置,第一对指针保存着最长的数字串,第二对指针保存当前找到的数字串,通过指针相减可以得到字符串长度,通过比对长度大小不断更新指针的位置。实现代码如下:

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
#include <iostream>
#include <string>
using namespace std;

int main()
{
string str;
while (cin >> str) {
bool numeric;
int long_start, long_end, long_len;
int tmp_start, tmp_end;

long_start = long_end = -1;
long_len = 0;
tmp_start = tmp_end = 0;

numeric = false;
int i;
for (i=0; i<str.size(); i++) {
if (str[i] >= '0' && str[i] <= '9') {
if (numeric == false)
tmp_start = i;
numeric = true;
} else {
if (numeric == true) {
tmp_end = i;
if ((tmp_end - tmp_start) >= long_len) {
long_start = tmp_start;
long_end = tmp_end;
long_len = tmp_end - tmp_start;
}
}
numeric = false;
}
}
if (i == str.size() && numeric == true) {
tmp_end = i;
if ((tmp_end - tmp_start) >= long_len) {
long_start = tmp_start;
long_end = tmp_end;
long_len = tmp_end - tmp_start;
}
}

if (long_len > 0) {
for (i=long_start; i<long_end; i++)
cout << str[i];
cout << "," << long_len << endl;
} else {
cout << ",0" << endl;
}
}

return 0;
}

题目二 字节流解析

问题描述

根据数值占用BIT数,按顺序从输入字节流中解析出对应数值,解析顺序按输入数组astElement索引升序

1
2
void Decode(unsigned int uiInputLen, unsigned char aInputByte[],
unsigned int uiElementNum, ELEMENT_STRU astElement[]);

其中

1
2
3
4
5
6
7
8
9
unsigned int uiInputLen : 字节数组 (流) 长度
unsigned char aInputByte : 字节数组 (流)
unsigned int uiElementNum : 解析数值个数
ELEMENT_STRU astElement[] : 数值的结构数组指针,含义如下
Struct
{
unsigned int uiElementLength; //表示uiElementValue占用BIT数,范围1~32
unsigned int uiElementValue; //从字节流中按顺序解析的数值,用于输出
} ELEMENT_STRU;

输入描述

1
2
3
4
5
字节数组长度uiInputLen为3;
字节数组aInputByte[3]为{0x62, 0x80, 0x00},对应二进制为 "0110 0010, 1000 0000, 0000 0000";
解析数值个数uiElementNum为2;
数值[0]的值占4个bit,即 astElement[0].uiElementLength = 4;
数值[1]的值占5个bit,即 astElement[1].uiElementLength = 5;

输出描述

1
2
数值[0]的值为6,二进制为 "0110",即 astElement[0].uiElementValue = 6;
数值[1]的值为5,二进制为 "0010 1",即 astElement[1].uiElementValue = 5;

示例 1

输入

1
2
3
4
5
3
0x62 0x80 0x00
2
4
5

输出

1
2
6
5

解决方案

这道题目主要是对数字进行进制转换,输入为一个十六进制字符串,然后需要转换成对应的二进制字符串,我的做法是将字符串转换成十进制整数 (使用stoi函数),然后将这个十进制整数转化成二进制字符串,将转换后的二进制字符串保持到数组aInputByte,这里我不固定数组aInputByte大小,根据输入的参数uiInputLen动态设定数组大小,数据转换完毕后,调用Decode函数进行解析。

PS: 十六进制字符串转二进制字符串应该可以直接用映射关系来转换,因为每个十六进制字符对应的二进制是固定的。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <iostream>
#include <string>
using namespace std;

struct ELEMENT_STRU {
unsigned int uiElementLength;
unsigned int uiElementValue;
};

void Decode(unsigned int uiInputLen, unsigned char aInputByte[],
unsigned int uiElementNum, ELEMENT_STRU astElement[])
{
unsigned int uiElementValue, uiElementLength, pow2;
int start, end;
start = 0;
for (int i=0; i<uiElementNum; i++) {
uiElementLength = astElement[i].uiElementLength;
uiElementValue = 0;
pow2 = 1;
end = start + uiElementLength;
for (int j=end-1; j>=start; j--) {
if (aInputByte[j] == '1')
uiElementValue = uiElementValue + pow2;
pow2 = 2 * pow2;
}
astElement[i].uiElementValue = uiElementValue;
start = end;
}
}

int main()
{
string str;
unsigned int uiInputLen, num, uiElementNum,flag;
unsigned int numberBytes;
int start, end;
unsigned char *aInputByte = NULL;
ELEMENT_STRU *astElement = NULL;
while (cin >> uiInputLen) {
aInputByte = (unsigned char *)malloc(sizeof(unsigned char) * 32 * uiInputLen);
start = 0;
end = 0;
for (int i=1; i<=uiInputLen; i++) {
cin >> str;
if (str.size() == 3) // 0xX
numberBytes = 4;
else if (str.size() == 4) // 0xXX
numberBytes = 8;
else if (str.size() == 5) // 0xXXX
numberBytes = 12;
else if (str.size() == 6) // 0xXXXX
numberBytes = 16;

end = start + numberBytes;

flag = 1;
num = stoi(str, 0, 16);
for (int j=end-1; j>=start; j--) {
if (flag & num)
aInputByte[j] = '1';
else
aInputByte[j] = '0';
flag = flag << 1;
}

start = end;
}

cin >> uiElementNum;
astElement = (ELEMENT_STRU*)malloc(sizeof(ELEMENT_STRU)*uiElementNum);
for (int i=0; i<uiElementNum; i++) {
cin >> num;
astElement[i].uiElementLength = num;
}

Decode(uiInputLen, aInputByte, uiElementNum, astElement);

for (int i=0; i<uiElementNum; i++)
cout << astElement[i].uiElementValue << endl;

free(aInputByte);
free(astElement);
}
return 0;
}

题目三 长整数相乘

问题描述

长整数相乘

输入描述

1
输入两个长整数,以空格隔开

输出描述

1
输出相乘后的结果

示例 1

输入

1
2
-12341234
43214321

输出

1
-533318047612114

解决方案

LeetCode有类似大整数相乘题目 43. Multiply Strings,和本题区别在于,LeetCode那题假设两个整数都是非负的,这题的整数可以是负数,在LeetCode 43题解题基础上加个符号判断即可解出该题。

问题描述

Write a function to delete a node (except the tail) in a singly linked list, given only access to that node.

Supposed the linked list is 1 -> 2 -> 3 -> 4 and you are given the third node with value 3, the linked list should become 1 -> 2 -> 4 after calling your function.

题目的意思是给单链表的节点指针 (不包括尾节点),然后删除该节点。

解决方案

如果按照常规单链表删除节点的操作,首先要找到要删除节点的前一个节点,然后再删除节点,但是这里只给定了待删除节点的指针,因此按照常规删除方法是不可行的,虽然待删除节点的前一节点我们不可知,但是我们可以知道该节点的下一个节点,如果我们把下一节点的内容复制到需要删除的节点上覆盖原有的内容,再把下一节点删除,那是不是就相当于把当前需要删除的节点删除了?根据题目描述,题目提供的节点不包括尾节点,因此不用考虑下一节点是 NULL 的情况。按照上面思路,可以有如下代码:

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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/

class Solution
{
public:
/*
* the node is not the last node
*/
void deleteNode(ListNode* node)
{
if (node == NULL)
return;

ListNode *next_node = node->next;
node->val = next_node->val;
node->next = next_node->next;
delete next_node;
}
};

在介绍排序算法之前,先约定数组表示为 $A$,大小为 $N$,数组下标从 $0$ 开始,排序的数组是从小到大排列

插入排序

在数组是反序的情况下,时间复杂度为 $O(N^2)$,在数组已排序的情况下,时间复杂度为 $O(N)$

1
2
3
4
5
6
7
8
9
10
11
void insertion_sort(int *nums, int n)
{
int p, tmp;
for (int i=1; i<n; i++)
{
tmp = nums[i];
for (p=i; p > 0 && nums[p-1] > tmp; p--)
nums[p] = nums[p-1];
nums[p] = tmp;
}
}

归并排序

时间复杂度为 $O(NlogN)$

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
void merge(int *nums, int *tmp, int left_start, int right_start, int right_end)
{
int left_pos, right_pos, pos;

left_pos = left_start;
right_pos = right_start;
pos = left_start;
while (left_pos < right_start && right_pos <= right_end) {
if (nums[left_pos] <= nums[right_pos])
tmp[pos++] = nums[left_pos++];
else
tmp[pos++] = nums[right_pos++];
}

while (left_pos < right_start) {
tmp[pos++] = nums[left_pos++];
}
while (right_pos <= right_end) {
tmp[pos++] = nums[right_pos++];
}

// copy array tmp back to array nums
for (int i=left_start; i<=right_end; i++)
nums[i] = tmp[i];
}

void msort(int *nums, int *tmp, int left, int right)
{
if (left < right)
{
int center = (left + right) / 2;
msort(nums, tmp, left, center);
msort(nums, tmp, center+1, right);
merge(nums, tmp, left, center+1, right);
}
}

void merge_sort(int *nums, int n)
{
int *tmp = new int[n];
msort(nums, tmp, 0, n-1);
delete[] tmp;
}

快速排序

平均时间复杂度为 $O(NlogN)$,最坏的情形下,时间复杂度为 $O(N^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
32
33
34
35
36
37
int partition(int *nums, int left, int right)
{
int i = left - 1;
int tmp;
int pivot = nums[right];

for (int j=left; j<=right-1; j++) {
if (nums[j] <= pivot) {
i = i + 1;

tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}

i = i + 1;
tmp = nums[i];
nums[i] = pivot;
nums[right] = tmp;

return i;
}

void qsort(int *nums, int left, int right)
{
if (left < right) {
int p = partition(nums, left, right);
qsort(nums, left, p-1);
qsort(nums, p+1, right);
}
}

void quick_sort(int *nums, int n)
{
qsort(nums, 0, n-1);
}

问题描述:

Given an array of integers, return indices of the two numbers such that they add up to a specific target.

You may assume that each input would have exactly one solution, and you may not use the same element twice.

Example:

Given nums = [2, 7, 11, 15], target = 9. Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1].

时间复杂度为 O(n) 解法

首先分析下问题,根据问题描述,可以得知数组是无序的。如果用暴力方法解决问题,也就是先在数组中固定一个数字,再判断数组中其余的 $n-1$ 个数字与它的和是不是等于目标值,这样时间复杂度是 $O(n^2)$。这里关键地方就是在于固定一个数字时,如何快速在数组中寻找与它的和等于目标值,而不是通过遍历方法一个个找,这样时间复杂度要 $O(n)$,想到快速查找,第一时间可以想到的是哈希表 (Hash Map),因为这个查找元素的时间复杂度为 $O(1)$。

对于哈希表,我们可以用数组中的数字作为哈希表的key,用数字的下标作为哈希表的value。当我们从头遍历数组时,对于访问到的数字,我们首先判断哈希表中是否存在与它的和等于目标值的key,如果不存在,插入该数字到哈希表中,如果存在,算法返回两个数字对应的数组的下标。可以看出,这个算法只需要遍历一次数组,因此算法复杂度为 $O(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
#include <unordered_map>
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> nums_map;
unordered_map<int, int>::const_iterator iter;
vector<int> indices;
for (int i=0; i<nums.size(); i++) {
if ((iter = nums_map.find(target - nums[i])) == nums_map.end()) {
nums_map.insert(make_pair(nums[i], i));
} else {
indices.push_back(iter->second);
indices.push_back(i);
break;
}
}

return indices;
}
};

int main()
{
int target = 9;
vector<int> nums;
nums.push_back(2);
nums.push_back(7);
nums.push_back(11);
nums.push_back(15);

Solution sol;
vector<int> indices = sol.twoSum(nums, target);
if (indices.size() == 0) {
cout << "Not found" << endl;
} else {
cout << "Find: [" << indices[0] << ", " << indices[1] << "]" << endl;
}
return 0;
}

问题描述:

Given an array of integers, every element appears twice except for one. Find that single one.

Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?

时间复杂度为 O(n) 算法

算法要用到异或运算,异或运算 的规律如下所示:

0 xor 0 = 0

0 xor 1 = 1

1 xor 0 = 1

1 xor 1 = 0

很容易验证一个异或运算的性质:任何一个数字异或它自己都等于0,试想有个数组 $[A, B, A, C, B]$,数字 $A$ 和数字 $B$ 重复出现两次,数字 $C$ 只出现一次,如果我们将数组所有数字进行异或运算,会出现什么情况?首先,可以写出一个表达式
$$
A \text{ xor } B \text{ xor } A \text{ xor } C \text{ xor B}
$$

由于异或运算满足交换律和结合律,因此可以得到下面这个表达式
$$
(A \text{ xor } A ) \text{ xor } ( B \text{ xor } B) \text{ xor C}
$$
由于 $A \text{ xor } A = 0$ 以及 $B \text{ xor } B = 0$,并且0与任何数异或都为原来的数,因此上面的表达式最终化简为
$$
(A \text{ xor } A ) \text{ xor } ( B \text{ xor } B) \text{ xor C} = C
$$
可见,对数组所有数进行异或运算,最终结果就是那个只出现一次的数。知道算法原理,我们可以很容易写出实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
int singleNumber(vector<int>& nums) {
int ret = 0;
for (auto it=nums.begin(); it!=nums.end(); ++it)
ret ^= *it;

return ret;
}
};

int main()
{
Solution solu;

vector<int> v1 = {2, 3, 2, -1, 3};
cout << "Single Number is: " << solu.singleNumber(v1) << endl;

return 0;
}