阅读视图

发现新文章,点击刷新页面。
☑️ ⭐

CSPJ 教学总结:深度优先搜索(DFS)

深度优先搜索(DFS)是学生学习算法的第一道门槛,因为它的主要形式是递归。而递归中需要将搜索的相关信息通过参数传递,这一点需要学生深刻理解 DFS。

模版

DFS 有比较标准的模版,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void dfs(int pt) // pt 表示层数
{
if (终止条件) {
// 处理
return;
}
for (枚举这个层的所有可选项) {
if(这个选项是合法的){
标记这个选项(保存现场)
dfs(pt+1);
取消标记(恢复现场)
}
}
}

我们将运用该模版,完成后面的题目。

递归的深度

递归的时候,程序会占用栈空间来保存函数的环境变量。根据编译器以及编辑参数的不同,栈空间的大小也不同。通常情况下,竞赛中的编译器设定的栈空间为 8M 或者 16M。

假如,我们在一个递归函数中使用了 10 个 int 变量,那么每个递归函数就需要 4*10=40字节的栈空间。8M 一共可以支持 8*1000*1000/40=200000层调用。考虑到栈空间还需要保存当前函数执行的地址等变量,可供支持的调用层数会更小一点。

同学们也可以做如下的递归函数栈空间的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int dfs(int x) {
int test[9] = {0};
cout << x << endl;
dfs(x + 1);
return 0;
}

int main() {
dfs(0);
return 0;
}

在我的本地,以上程序调用了约 13 万次后栈溢出。为了保险,我们在比赛中如果调用深度小于 1 万层,那应该是稳妥的;否则我们需要考虑是否用别的解法来解题。

教学和练习题目

题目名说明
P1036 选数NOIP 2002 普及组
P1219 八皇后 Checker ChallengeUSACO 1.5
P1596 Lake Counting SUSACO10OCT
P2036 PERKETCOCI 2008/2009 #2
P12139 黑白棋蓝桥杯 2025 省 A,写起来较繁琐
P1605 迷宫标准的 DFS
P2404 自然数的拆分问题
P1019 单词接龙NOIP 2000 提高组

P7200
P10483

P1219 八皇后 Checker Challenge

这是八皇后的变种,N 皇后问题。可以作为基础练习。具体解法是:

  • 我们用变量 v[15] 表示每个皇后的列值。
  • 对于新放入的皇后,我们依次检查它与前面的皇后是否在一条斜线上。检查方法是看其“横坐标差”与“纵坐标差”是否相同。检查函数如下:
1
2
3
4
5
6
bool check(int pt) {
for (int i = 0; i < pt; i++) {
if (abs(v[i] - v[pt]) == abs(i - pt)) return false;
}
return true;
}

完整的代码如下:

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
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int n;
int v[15], ans;
bool flag[15];

bool check(int pt) {
for (int i = 0; i < pt; i++) {
if (abs(v[i] - v[pt]) == abs(i - pt)) return false;
}
return true;
}

void dfs(int pt) {
if (pt == n) {
ans++;
if (ans <= 3) {
for (int i = 0; i < n; i++) {
cout << v[i] << " ";
}
cout << endl;
}
return;
}
for (int i = 1; i <= n; i++) {
if (flag[i]==false) {
flag[i] = true;
v[pt] = i;
if (check(pt)) dfs(pt + 1);
flag[i] = false;
}
}

}

int main() {
cin >> n;
dfs(0);
cout << ans << endl;
return 0;
}

P1036 选数

此题需要从小到大取数求和,然后再判断是否是素数。用递归的方式来进行枚举。

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
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int n, k, tot, ans;
int a[22], p[22];

bool isPrime(int v) {
for (int i = 2; i*i <= v; ++i) {
if (v%i == 0) {
return false;
}
}
return true;
}

void dfs(int pt) {
if (pt == k+1) {
if (isPrime(tot)) {
ans++;
}
} else {
// 每一层都必须取比前一层更大的下标,防止重复取
for (int i = p[pt-1]+1; i <= n; ++i) {
p[pt] = i;
tot += a[i];
dfs(pt+1);
tot -= a[i];
}
}
}

int main() {
cin >> n >> k;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
dfs(1);
cout << ans << endl;
return 0;
}

P1596 Lake Counting S

此题既可以用 DFS,也可以用 BFS。考虑到 N 和 M 最大值为 100,所以递归的层次最多为 1 万层,所以我们可以试试 DFS。

以下是参考代码:

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
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;
int n, m;
char tu[105][105];
int ans;
int movex[8] = {0, 0, 1, -1, 1, 1, -1, -1};
int movey[8] = {1, -1, 0, 0, 1, -1, 1, -1};

void dfs(int x, int y) {
tu[x][y] = '.';
for (int i = 0; i < 8; i++) {
int nx = x + movex[i];
int ny = y + movey[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m
|| tu[nx][ny] != 'W') continue;
dfs(nx, ny);
}
}

int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> tu[i][j];
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (tu[i][j] == 'W') {
dfs(i, j);
ans++;
}
}
}
cout << ans << endl;
return 0;
}

P2036 PERKET

因为 N 最多为 10,每种食材可以选或者不选两种情况,所以最多情况数为 2^10=1024 种。搜索时间满足要求。

所以,此题用 DFS 可以非常方便解决。在搜索的时候,我们可以将食材的相关信息带到 DFS 函数的参数中,方便最后答案的求解。

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
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int n;
int s[11], b[11], v[11];
int ans = INT_MAX;

/**
* pt: 当前处理到的食材
* cnt: 当前选中的食材数量
* ss: 当前选中的食材的总酸度
* bb: 当前选中的食材的总甜度
*/
void dfs(int pt, int cnt, int ss, int bb) {
if (pt == n) {
if (cnt > 0) {
ans = min(ans, abs(ss - bb));
}
return;
}
v[pt] = 1;
dfs(pt + 1, cnt + 1, ss * s[pt], bb + b[pt]);
v[pt] = 0;
dfs(pt + 1, cnt, ss, bb);
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> s[i] >> b[i];
}
dfs(0, 0, 1, 0);
cout << ans << endl;
return 0;
}

P12139 黑白棋

此题是搜索题,需要在中间尽可能检查状态来剪枝,以节省搜索次数。

题目有三类限制,分别可以用在不同的剪枝环节。

限制一:在每一行和每一列中,黑色棋子和白色棋子的数量必须相等(即为 3)。

  • 我们可以对每一行记录黑子和白子的数量,如果某一行或某一列的一种颜色达到 3,后面则不能用这个颜色。

限制二:不能有超过两个相同颜色的棋子连续排列。

  • 我们可以在当前落子的时候,检查它的左边和上面连续的几个格子,看是否有 3 个相同的子。

限制三:行列唯一性

  • 可以放到最后检查。

另外,这个棋盘有几个位置已经设定了值,我们需要标记下来,搜索的时候跳过对这些位置的尝试,但需要在这些位置做合法性检查。

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
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int row_cnt[6][2], col_cnt[6][2];
char tu[7][7], mark[7][7];

bool check(int r, int c) {
// 在每一行和每一列中,黑色棋子和白色棋子的数量必须相等(即为 3)
if (row_cnt[r][1] > 3 || row_cnt[r][0] > 3 || col_cnt[c][1] > 3 || col_cnt[c][0] > 3) return false;

// 不能有超过两个相同颜色的棋子连续排列
if (r >= 2) {
if (tu[r][c] == '1' && tu[r-1][c] == '1' && tu[r-2][c] == '1') return false;
if (tu[r][c] == '0' && tu[r-1][c] == '0' && tu[r-2][c] == '0') return false;
}
if (c >= 2) {
if (tu[r][c] == '1' && tu[r][c-1] == '1' && tu[r][c-2] == '1') return false;
if (tu[r][c] == '0' && tu[r][c-1] == '0' && tu[r][c-2] == '0') return false;
}
return true;
}

// 行列唯一性检查
bool final_check() {
set<int> row_set, col_set;
for (int i = 0; i < 6; i++) {
int v = 0;
for (int j = 0; j < 6; ++j) {
v = v * 10 + (tu[i][j] - '0');
}
row_set.insert(v);
}
if (row_set.size() != 6) return false;
for (int j = 0; j < 6; ++j) {
int v = 0;
for (int i = 0; i < 6; ++i) {
v = v * 10 + (tu[i][j] - '0');
}
col_set.insert(v);
}
if (col_set.size() != 6) return false;
return true;
}

void dfs(int r, int c);
void try_dfs(int r, int c) {
char ch = tu[r][c];
row_cnt[r][ch - '0']++;
col_cnt[c][ch - '0']++;
if (check(r, c)) {
int nr = r;
int nc = c + 1;
if (nc == 6) {
nr++;
nc = 0;
}
dfs(nr, nc);
}
row_cnt[r][ch - '0']--;
col_cnt[c][ch - '0']--;
}

void dfs(int r, int c) {
if (r == 6) {
if (final_check()) {
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 6; j++) {
cout << tu[i][j];
}
}
cout << endl;
// 因为只有一个合法解,所以找到答案就退出
exit(0);
}
return;
}

if (mark[r][c] == 0) {
tu[r][c] = '1';
try_dfs(r, c);
tu[r][c] = '0';
try_dfs(r, c);
} else {
tu[r][c] = mark[r][c];
try_dfs(r, c);
}
}

void init() {
memset(mark, 0, sizeof(mark));
mark[0][0] = '1';
mark[0][1] = '0';
mark[0][3] = '0';
mark[1][3] = '0';
mark[2][4] = '0';
mark[2][5] = '0';
mark[4][2] = '1';
mark[4][5] = '1';
mark[5][1] = '0';
mark[5][4] = '1';
}

int main() {
init();
dfs(0, 0);
return 0;
}

P1605 迷宫

用 DFS 来枚举,但需要标记走过的路。

  • 因为最多只有 5x5=25 个格子,所以递归的深度最大只有 25,不存在溢出情况。
  • 因为有陷阱(不能走)和起点终点(不能重复走),所以我们假设平均每次有 2 条支路,
    整个的最坏情况估计只有 2^23=8388608 次,所以也不会超时。

一些陷阱:

  • 终点可能也有障碍物,这个时候始终就到不了。
  • 起点在走之前需要标记,否则会重复走。

参考代码:

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
#include <bits/stdc++.h>
using namespace std;

// 0 - 空地
// 1 - 障碍物
int tu[6][6], n, m, t, sx, sy, ex, ey, ans;

int movex[]={1,-1,0,0};
int movey[]={0,0,-1,1};

void dfs(int x, int y) {
if (x == ex && y == ey) {
ans++;
return;
}
for (int i = 0; i < 4; ++i) {
int tox = x + movex[i];
int toy = y + movey[i];
if (tox >=1 && tox<=n && toy>=1 && toy<=m && tu[tox][toy]!=1){
tu[tox][toy]=1;
dfs(tox, toy);
tu[tox][toy]=0;
}
}
}

int main() {
ios::sync_with_stdio(false);
cin >> n >> m >> t;
cin >> sx >> sy >> ex >> ey;
while (t--) {
int x, y;
cin >> x >> y;
tu[x][y] = 1;
}
tu[sx][sy] = 1;
dfs(sx, sy);
cout << ans << endl;
return 0;
}

P2404 自然数的拆分问题

DFS,有两个技巧:

  • 保证后面的数 >= 前面的数。
  • 让每个数必须小于 n,这样不会出现 n=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
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

int n, tot, v[10];

void dfs(int pt) {
if (tot == n) {
cout << v[1];
for (int i = 2; i < pt; ++i) {
cout << "+" << v[i];
}
cout << endl;
return;
}
for (int i = v[pt-1]; tot + i <=n && i < n ; ++i) {
tot += i;
v[pt] = i;
dfs(pt+1);
tot -= i;
}
}

int main() {
ios::sync_with_stdio(false);
cin >> n;
v[0] = 1;
dfs(1);
return 0;
}
☑️ ⭐

CSPJ 教学思考:数学题

数学题是信息学竞赛中重要的一类题目,通常包括几何、数论、容斥原理等。

本文将相关的题目归纳整理,用于教学。

质数相关

判断一个数是否为质数

此算法是很多数学相关题目的基础,在 GESP 二级中也有涉及。例如:B3840 找素数

其核心代码是:

1
2
3
4
5
6
bool isPrime(int a) {
for (int i = 2; i*i <=a; i++) {
if (a%i == 0) return false;
}
return true;
}

初学者在写的时候,要注意 i*ia 的比较是小于等于。

质因数分解

质因数分解的方法是从 2 开始试商,如果发现能整除,就把被除数中该因数去掉,关键代码是:

1
while (N % i == 0) N /= i;

这样经过几轮下来,N 的值会变得很小,最后 N 如果不为 1,N 就是最后一个质因数。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
vector<int> prime_facs(int N) {
vector<int> result;
for (int i = 2; i * i <= N; i++) {
if (N % i == 0) {
while (N % i == 0) N /= i;
result.push_back(i);
}
}
if (N != 1) { // 说明再经过操作之后 N 留下了一个素数
result.push_back(N);
}
return result;
}

练习题:

B3969 GESP202403 五级 B-smooth 数 的参考代码:

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
#include <bits/stdc++.h>
using namespace std;

int n, b, ans;

int getMaxPrime(int v) {
int ret = 0;
for (int i = 2; i*i <= v; i++) {
if (v%i == 0){
ret = max(ret, i);
while (v%i == 0) v/=i; // 把 v 的值缩小
}
}
ret = max(ret, v);
return ret;
}

int main() {
cin >> n >> b;
for (int i = 1; i <=n; ++i) {
int t = getMaxPrime(i);
if (t <= b) ans++;
}
cout << ans << endl;
return 0;
}

几何

P2241 统计方形

本题解法:每个矩形(包括正方形)都可以由一段左边线段和一段上边线段确定。因此,我们只需要枚举所有可能的线段。

对于一个长是 N 宽是 M 的棋盘。

  • 左边的线段长度为 1 的有 N 个,长度为 2 的有 N-1 个,…长度为 N 的有 1 个。
  • 上边的线段长度为 1 的有 M 个,长度为 2 的有 M-1 个,…长度为 M 的有 1 个。

所以:

  • 左边的线段一共有 (1+2+3+...+N)= N*(N+1)/2 个。
  • 上边的线段一共有 (1+2+3+...+M)= M*(M+1)/2 个。
  • 因此,总共有 N*(N+1)/2 * M*(M+1)/2 个矩形。

用相同的办法可以推导正方形的数量,方法如下:

  • 对于左边长度为 1 的线段有 N 个,相应的上边长度为 1 的线段有 M 个。
  • 所以可以构造出 N*M 个边长为 1 的正方形。

同理:

  • 对于左边长度为 2 的线段有 N-1 个,相应的上边长度为 2 的线段有 M-1 个。
  • 所以可以构造出 (N-1)*(M-1) 个边长为 2 的正方形。

以此类推,可以构造出 N*M + (N-1)*(M-1) + (N-2)*(M-2) + (N-M+1)*1 个正方形(N>M)。

另外,需要注意使用 long long 来保存结果。完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <bits/stdc++.h>
using namespace std;
unsigned long long n, m, ans1, ans2;
int main() {
cin >> n >> m;
ans1 = n*(n+1)/2 * m*(m+1)/2;
while (n > 0 && m > 0) {
ans2 += n*m;
n--; m--;
}
cout << ans2 << " " << ans1 - ans2 << endl;
return 0;
}

数论

P1044 栈

这道题可以先用暴力的办法把前面几个数打出来,然后我们能发现数的规律是:1,1,2,5,14,42,132,429,1430,….

这是计算组合中很常见的卡特兰数,卡特兰数有两种公式,第一种公式是:

  • f(n) = f(n-1) * (4 * n - 2) / (n + 1)

我个人觉得这个公式不太好记。另一个公式是:

这个递推式相对好记一点:即C(n) = C(0)*C(n-1) + C(1)*C(n-2) ... C(n-1)*C(0)

以下是用该递推式实现的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Author: Tang Qiao
*/
#include <bits/stdc++.h>
using namespace std;

long long ans[19];
int main() {
int n;
cin >> n;
ans[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; ++j) {
ans[i] += ans[j] * ans[i-1-j];
}
}
cout << ans[n] << endl;
return 0;
}

P3612 USACO17JAN Secret Cow Code S

这是一道 USACO 的题目,需要我们先找出规律,然后再试图求解。

此题找规律的技巧是分析坐标每次在折半还原时的变化规律。
为了分析规律,我们可以看每次翻倍时,坐标的关系变化。

对于一个长度为 N 的字符串S,每次其长度变为 2*N。所以,我们对每一位进行标号:

1 2 3 4... N N+1 N+2 N+N

其中,除 S[N] == S[N+1] 外(按题意,此项为特殊项),其它位置都符合如下规律:

  • S[1] == S[N+2]
  • S[N-1] == S[N+N]

所以,将右边的坐标减去 N 再减 1,就得到左边的坐标。

所以,设总长为 L, 如果 a 的位置在右半侧,则对应到左半侧的坐标关系是:

  • if (a == L/2+1) a = 1;
  • else a = a - L/2 - 1;

如此递归下去,直到位置落在最初的长度上。
因为字符下标是从 0 开始,所以下标最后要减 1.

最后注意用 long long 来转换坐标。

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

#include <bits/stdc++.h>
using namespace std;

string s;
long long a, n;
bool debug = false;

long long di(long long a, long long L) {
if (debug) {
// 可用 debug 查看坐标变化过程
cout << "test a = " << a << ", L = " << L << endl;
}
if (a <= n) {
return a;
} else {
// 如果 a 的位置在右半侧,则调整到左半侧
if (a > L/2) {
if (a == L/2 + 1) a = L/2;
else a = a - L/2 - 1;
}
return di(a, L/2);
}
}

int main() {
cin >> s >> a;
n = s.length();

// 求出开始往回递归时,字符串拼起来的长度 L
long long L = n;
while (L < a) L *= 2;

// 寻找 L 这个长度下,第 a 个字符相当于哪个位置
int ans = di(a, L);
cout << s[ans-1] << endl;
return 0;
}
☑️ ⭐

如何控制孩子的电脑使用

背景和问题

小学生在学习编程的时候,我们必然需要使用电脑上机练习。但是,电脑上也充满了各种“诱惑”:

  • 打开网页无处不在的游戏广告,很多游戏还是网页游戏
  • 应用市场里各种各样的游戏
  • 小红书,B 站等各种各样的网站也充满吸引力

那我们如何保证孩子能够在上机的时候一直专心练习编程呢?难道得一直在旁边盯着吗?

为此,我做了一些功课,分享给大家。

解决方案(Windows 平台)

微软的 Windows 操作系统中有一个家长控制功能。通过该功能家长可以限制小朋友对计算机功能的使用,以及规定和限制使用 Windows 的某些功能。

例如: 限制孩子的账户只能使用某个应用程序、游戏等。

使用 Windows 的家长控制功能可以在不安装其它软件的情况下,控制孩子使用Windows的绝大部分应用和功能。

具体操作方式如下。

1、为孩子创建一个单独账号

  • 按下键盘上的“Windows”键+“I”键打开设置→点击“账户”
  • 点击左侧的“账户/家庭和其他用户”,并“添加账户”
  • 在弹出的窗口中点击“为孩子创建一个”,按步骤创建新的Microsoft账户
  • 用新建的账户登录,在“概述”里面的隐私设置里打开“共享我的活动”,如下图

2、在线管理家庭设置

  • 用家长账户重新登录电脑
  • 再次按下“Windows”键+“I”键打开设置→点击“账户”
  • 点击左侧的“账户/家庭和其他用户”
  • 点击“在线管理家庭设置或删除账户”打开管理链接

在管理链接中就可以管理孩子的时间了。

解决方案(Mac 平台)

  • 1、为孩子单独注册一个 Apple ID。
  • Mac 平台的家庭共享功能可以将孩子加入到一个家庭中。
  • 可以在家庭共享中进入到孩子的帐户,查看孩子的屏幕使用时间,以及限制一些功能的使用。如下图:

以上。

☑️ ⭐

人单合一 - 读《永恒的活火》

最近读完了海尔的创始人张瑞敏的《永恒的活火》。张瑞敏从 1984 年创立海尔以来,将海尔从一个小厂变成了一家世界 500 强企业,这本书主要介绍了他的“人单合一”的管理理念。

以下是一些读书总结。

一、什么是人单合一

人单分别指的是人:员工;单:用户。

人单合一的核心思想是将管理模式面向员工和用户做调整,以便调动员工最大的工作积极性,激发产品创新力。同时用市场化的方式来检验团队的成果,让大家关注点都集中在产品售卖和用户体验上。

具体来说,在人单合一的模式下,员工是以混合业务小组(书中把它称作链群合约的小微)存在的,小组内部通过自发的选拔来形成小组长,小组长背负大家认可的绩效目标,同时业务如果发展得好,小组长也可以获得更大的经济收益。

书中并没有详细介绍混合业务小组的职位构成,但强调产品和销售同事都需要为最终的售卖结果负责。

在用户方面,人单合一讲求让小组自己发现和尝试产品机会。把小组变成一个个产品创新的实验田。在评价产品的时候,组织用纯市场的方式来评估,即只有销量好的产品才是真正命中市场需求的产品。

如何评价好的产品,作者引入了很多价值维度。强调给用户的体验增值作为核心评价指标。

对于职能化的团队,张瑞敏把它变成了为小组赋能的中台单元。这种更像是服务的中台限制了职能团队对业务的话语权,使得业务小组的权力更大。

另外张瑞敏砍掉了很多中层领导,让一线员工自己做决策。

二、本书的优点

作为普通人很难接触到世界 500 强的 CEO 的系统性观点。张瑞敏作为在中国成长起来的企业家,他的管理观点融入了中国的传统文化,更容易被大家理解和接受。

本书的核心管理观点“人单合一”也特别适合当下这个信息爆炸的时代。《构建网状组织架构 - 赋能》一书里面也对现代组织有类似的观点,只是角度不一样。

另外,张瑞敏在书中也提及了很多管理大师的观点和著作,可以作为补充阅读材料。

三、本书的缺点

这本书虽然作者是张瑞敏,但是其实是张瑞敏和其他管理学者的访谈录。作为访谈录这本书有两个缺点:

1、全书都是张瑞敏参加线下访谈的内容,而访谈双方也都很友善,所以并没有什么特别不同的观点碰撞。

其实访谈如果像李翔那样做得很有深度也很好,但是像本书这样的访谈,由于双方都不是专业做访谈的,所以提问的结构,对受访者回答的引导和深挖都显得不够专业。

2、这本书有足足 600 页厚,最主要的观点就是“人单合一”。全书的访谈中多次重复提到这个观点,但是对观点的阐述多集中在原理层面,但在执行层面上的难度,张瑞敏虽然有多次提及,但是却没有系统的说明。

一些有助于人单合一的管理工具(例如人单合一计分卡、共赢增值表),张瑞敏在书中也是点到为止,没有从设计角度解释这些工具的产生过程和设计思路。

这就造成了张瑞敏虽然在 20 年前的 2005 年就提出了“人单合一”,但是其管理思想的讨论和实践仍然不流行。

以上。

☑️ ⭐

供应链笔记(9):开源岗位

一、开源的工作定义

开源的工作主要包括:

  • 为业务寻找合格的物料供应商
  • 为物料的初始价格进行商务谈判,保证价格在合理范围内
  • 为新供应商的顺利引入努力,包括核对相关的资质,对接打样,生产周期
  • 供应商成功引入后,将供应商顺利交接给采购

二、开源的工作挑战

2.1 开源的工作挑战一:寻找到足够多的合格供应商

供应链的价格合理,供应安全都建立在足够的合格供应商库中,如果合格供应商不足(例如只有单一供应商),那么就可能会出现:

  • 价格不合理。供应商如果找个借口坐地起价,为了保证供应安全,你很多时候在没有合适替代品的下,在权衡利弊之后,也可能忍痛短时间接受涨价。
  • 标准打折扣。供应商生产的商品有轻微瑕疵。为了保证供应安全,你只能被迫允收。
  • 交付延期。供应商可能延期交货,对此你无法有效监管和惩罚。

对于一个采购品,如果我们有多个合格供应商,那么我们即便对这个采购品的价格分析不够深入,我们也可以靠简单地多方竞争报价的方式,让供应商的价格回归到一个正常的合理水平。

2.2 开源的工作挑战二:保证合格供应商来源的丰富性

多个合格供应商如果背后串通,那么在价格上就会产生围标行为,这对供应链来说是灾难性的。所以开源需要定期从不同的源头来寻找新的合格供应商,以保证合格供应商池的丰富度和新鲜度。

只有在源头上保证供应商的不断引入,才能有效减少围标行为的产生。为此,斑马供应链要求对于年采购额大于 1000 万的品,每年都要争取开新的供应商源,用小批量的试产和议价,保证价格的合理性和供应的安全性。

2.3 开源的工作挑战三:保持廉洁

开源岗位很大程度上决定了引入哪些供应商,如果和供应商串通,对供应链影响极大。

除了在制度上要求开源来源的丰富性、引入供应商管理复核、多个开源岗位相互轮岗外,我们也要在各种环节引入内审的监督机制,定期督查供应商引入的细节,维持廉洁管理的压力。

三、开源岗位的要求

从事开源岗位需要具备如下素质:

  • 廉洁。能够抵挡住诱惑。
  • 积极主动。主动解决问题。
  • 优秀的沟通技巧。能够促成双赢的合作。
  • 专业的领域知识。能够有效评价供应商的能力。

以上。

☑️ ⭐

供应链笔记 (8):印刷的环节和成本

一、序言

印刷品的用处非常普遍,基本上各种产品都需要配套一些印刷品作为说明书或者教程。所以我们有必要了解印刷品的生产工艺和价格。

下面我先介绍一下印刷的生产环节。

二、印刷的环节

印刷的环节通常包括以下项目。

2.1 准备印刷文件

将客户给的印刷资料进行转码,检查字体有无缺失,对内容物进行拼版等。这个步骤通常使用印厂的专业软件完成。

2.2 制版

印刷的原理是将油墨涂到底版上,然后底版再像 “盖印章” 那样把油墨转印到纸张上。

所以,印刷前需要制作用于印刷的底版。这种版都是一次性的,所以如果一次印刷量大,摊薄下来的制版费就便宜。另外,每种颜色需要一种版,四色印刷(CMYK)就需要制四个版。

一张版的制作成本大概在 50 块钱左右。通常印厂的含税含利润报价在 70 以上。

制版的机器通常不太贵,在几十万这个价格范围。

制版的速度通常很快,一张版制完大概在 10 分钟左右,所以制版一般不是生产瓶颈。

2.3 印刷

印刷前其实一般还需要对纸张进行裁切,因为纸厂出厂的纸一般都是全开的,但是印刷机一般只能印半开。切好之后就可以印刷了。

印刷的机器分为双面印刷机和单面印刷机,像 4 色双面印刷机一次可以装 8 张版,完成半开纸双面的印刷工作。

印刷机的速度可以调到很高,一小时快的可以印 1 万张,慢的也能印 5 千张。

2.4 过油 / 覆膜

对印刷完的纸张,一般为了保护油墨不被划伤,可能还会增加过油或覆膜的工艺。这些工艺通常在一个起订量的基础上,按张数算钱。

2.5 折页

折页是将印刷好的一大张纸不断对折,变成目标大小。我们有时候可以在书的背面看到 16 开,其实就是指把全开的纸折 4 次,2^4=16。这种折页操作由专门的折页机来完成。

折页机的速度也非常快,每秒折 3 次非常常见。

2.6 配页 / 锁线

配页指将多个折好的折页重叠到一起。

锁线指将多个折好的折页用线缝到一起。

对应有相应的配页机器和锁线机器。

2.7 装订

装订一般分为骑马钉和锁线胶钉两种。大部分图书都是胶钉的。一些页数比较少的图书或者宣传册会选择用骑钉。

斑马阅读和英语的绘本因为比较薄,就选择的是骑马订的装订方式。

胶钉是在书脊涂上胶水,再粘上封面。因为胶水需要时间干,所以一般都需要一个稍长的传送带,让书籍在传送带上稍微多待一会儿,保证胶水干掉。

2.8 切成品和打包

图书在生产的最后环节是切掉四周的多余部分,这样使得折页的一些压痕处被彻底切开,保证书页之间没有粘连。

在产线的最后一般是打包。工人将生产好的图书打成小包,以便于运输。

三、成本的估算

虽然印刷的环节很多,但是每个环节一般都有相关的标准价格,业内俗称 “工价”。我们拿到一个印刷需求后,根据工序拆分带入相应的工价就可以算出来费用。

另外,我们还需要计算纸张的费用。纸张的价格也是可以通过一些公开渠道查询到。纸张的价格有一定的周期性,现在价格在 6000 一吨左右。但疫情期间,白卡纸最高涨到过 1 万多一吨。

估算一个印刷品纸张的成本最好的办法就是拿个食品秤称一下。拿我之前看的这本《置身事内》书来举例,它大概 500g 重,胶版纸大概 6000 一吨(合 0.006 一克) 那么这本书的纸张费用就是:0.006*500 = 3 块钱。

怎么估算工序的费用呢?详细算工价太麻烦了,我习惯的办法是把我们以前类似的产品工价与纸张的成本比例看一看,然后快速估算。

比如我们以前生产过的一本书,工价和纸张的比值是 1 比 1。那么就可以快速估算《置身事内》这本书整体成本就是 6 块。

四、小结

印刷品的生产包括准备印刷文件、制版、印刷、折页、装订等环节。其生产成本主要包括纸张成本和印刷工费。

估算成本的最简单办法是通过称重 + 通过历史经验数据来预估工费比例来快速获得成本范围。

☑️ ⭐

供应链笔记(5):注塑成本估算

交付和价格是供应链岗位日常工作的两大重点。这一篇谈谈与价格相关的工作:注塑成本估算。

注塑的费用构成在行业内比较透明,主要包括:机台费、电费、原料费、工人工资几大部分。

一、机台费

注塑机的品牌和吨位决定了注塑机的价格,我们通过注塑机的销售渠道或者二手交易网站可以查到对应型号的注塑机的价格。

我们将注塑机的价格按 5 年摊销,就可以算出每年的机台费用。假如注塑机的行业开机率 50%(即一年 180 天),拿注塑机每年的摊销除以 180 天,就可以算出来每天的机台费用。

拿一台国产的海天 35T 注塑机举例,假设售价是 20 万元,那么每天的机台费就是 200000/5/180=222.2元。如果注塑机每天工作 22 小时,那么每小时费用就是 10 元。

在计算单件费用的时候,需要考虑上成型时间。例如:某个一出一的模具,成型时间是 60 秒。那么一小时只能做 60 个。那单件的机台费就是 10/60=0.17 元。

二、电费

电费的成本占比其实还不少,我们可以查阅当地的工业用电电量,再检查注塑机的功率来计算电费情况。

不同吨位注塑机功率不一样,功率范围在 2 ~ 20 千瓦的都有。

比如某地工业用电是 2 块钱一度,注塑机的功率是 4 千瓦,那么每小时的电费是 8 元。一天 22 小时(假设每天检修2小时)就是 176 元。

通过了解每件壳料的成型时间,以及模具是1出几,就可以计算单件壳料消耗的电费。

例如:某个一出一的模具,成型时间是 60 秒。那么一小时只能做 60 个。如果注塑机的功率和电费按刚刚计算的是每小时 8块。那么每件该壳料的电费就是 8/60 = 0.13 元。

电费一般都高低峰的差价,如果要精确核算可以加权计算一下全天开机的成本。

三、原料费

注塑机通常使用 ABS 等原料,这些原料都有市场的吨价。我们可以通过注塑成型的壳料重量,来反推这部分的原料成本。

例如:假设 ABS 塑胶粒 2 万一吨(实际应该没那么贵,我是为了举例)。某产品注塑成型后的壳料重量为 200 克。则该产品的原料费为 20000 / 1000000 * 200 = 0.02 * 200 = 4 元

注意:如果注塑的模具是冷流道的,那么每次注塑还会有水口料存在。计算的时候需要把水口料的重量也计算在内。

一个复杂一点的例子:假设 ABS 塑胶粒成本是 2 万一吨。某个模具是 1 出 4 的形式生产,注塑出来的单件重量为 200 克,1出4的水口料为 100 克。那么单件的原料成本是多少?计算方式就是将每件的重量核算为 200 克 + 25 克的水口,一共 225 克,然后再像上面的方式计算成本。

如果有些供应商现场发现会添加一定比例的水口料在原料中,那么相应的也要减少水口料的成本。

当然,注塑过程还会有一定比例(例如3%)的损耗,但是因为占比较小,估算分时候可以忽略。

四、工人工资

根据不同产品的特点不同,有些产品需要在注塑出料位置安排工人进行处理,处理的常见步骤包括:修剪水口,外观检查,打包等等。

下图是一个例子,工人正在修剪水口和打包壳料,套袋后装入旁边的打包箱。

工人的工资可以通过打听来了解。通常深圳一带的工厂,工人工资每个月大概 6000 ~ 8000 块钱,日薪在 200 左右。所以每小时的工资在 25 元左右。工厂通常还包吃住,加上管理人员,成本应该会更高一点。

我们通过计算工人每小时能够处理的塑胶元件的数量,就可以估算出每件占用的人力成本。

注塑机的操作通常也需要懂技术的工人看管,但是一般情况下一个人可以同时看管多台。注塑机上下模具和测试的时候,也会比较耗费高级工人的精力,这部分的成本具体如何估算,要结合现场来判断。

五、利润

工厂不是慈善机构,也是要赚钱的。上面的成本计算以后,一般还是需要增加 10~20% 的利润,作为工厂的毛利。

六、小结

注塑成本估算包括机台费、电费、原料费、工人工资几大部分。

以上只是一个笼统的框架,不同的产品在具体估算的时候考虑重点不一样,需要具体案例具体分析。

以上。

☑️ ⭐

供应链笔记(1):如何挑选供应商

一、序

之前管过供应链,分享一些我的思考。

这是第 1 篇:如何挑选供应商

挑选供应商之前需要先开源和引入供应商,所以我们先从开源说起。

二、开源

开源供应商的办法有很多:朋友推荐、网上搜索、查目标产品包装上的生产厂家等。总之,你很容易就可以找到很多候选的供应商,然后你就可以进行下一步的拜访了。

三、拜访

拜访供应商我喜欢观察他们的生产产线,基本上管理到不到位一下子就可以看出来。即便是很小规模,优秀的供应商的产线也会是整洁有序的。

供应商的设备也体现出他们的实力。注塑机的吨位和品牌决定了供应商花了多少钱投在设备上,产能上限是多少。

接下来进入今天的正题:如何挑选供应商。

四、挑选

挑选供应商的最重要的原则是:匹配。

所以,苹果的供应商对你来说不一定就最好,大部分情况下,你是用不起苹果的供应商的。即使你想用,人家也不一定愿意接你的生意。

所以,根据你的产品的采购规模,去匹配合适的供应商最重要。最好是你们相互之间都看得上对方。比如:你的年采购额能够占到他年营业额的5%~20%,在这个区间内就会比较匹配。他会很在意和你的生意,你呢,也不会担心他产能不足无法完成你的订单交付。

对于年营业额在 2 亿以内的供应商,我建议只挑选那些老板亲自管理厂子的供应商。通常职业经理人在这种规模的厂里面都不太有老板的决策权力和投入度。

五、合作

合作上,应该在保证供应安全的原则下,尽可能把订单相对集中。这样可以对核心供应商有较强的利益捆绑。也容易让对方产生规模效应,从而达成降本的目标。

六、小结

  • 挑选和自己采购额匹配的供应商。
  • 挑选老板亲自管业务的供应商。
  • 保证供应安全的前提下,集中订单,发挥规模效应。

以上。

❌