模拟
有些问题的解题步骤,就是我们思考的步骤。这时可以模拟我们的思考,一步步解决问题。
约瑟夫环
问题:N个人围成一圈,从 1 开始报数,第M个出列,然后下一个人继续从 1 开始报数,直到最后剩下一个。
题目已经很清楚的给出了解题步骤,我们只需要构造一个循环链表(或者用数组,但需要注意越界时跳到数组的第一个元素来实现循环效果)。
数组
#include <stdio.h>
void mov(int arr[], int n, int cur) {
for (int i = cur; i< n - 1; i++) {
arr[i] = arr[i + 1];
}
}
void joseph(int n, int m, int ret[]) {
int arr[n];
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
int left = n;
int cur = 0;
while (left > 0) {
// for (int i = 0; i < m - 1; i++) {
// cur++;
// if (cur >= left) {
// cur = 0;
// }
// }
cur = (cur + m - 1) % left;
ret[n - left] = arr[cur];
mov(arr, left, cur);
left--;
if (cur >= left) {
cur = 0;
}
}
}
int main(void) {
int n = 7;
int m = 3;
int ret[n];
joseph(n, m, ret);
for (int i = 0; i < n; i++) {
printf("%d\t", ret[i]);
}
return 0;
}
链表
首先构造循环链表,然后开始数数,每次从1数到m后,剔除这个元素,开始下次数数。
注意,C 语言的结构体变量声明后会在栈空间分配一个固定内存空间。如果需要多个结构体,需要用 malloc 去堆空间申请内存。
#include <stdio.h>
#include <malloc.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
typedef struct List {
Node *head;
Node *tail;
} List ;
void joseph(int arr[], int n, int m, int ret[]) {
List list = {NULL, NULL};
int count = 0;
// build cycle linked list
Node *tmp = (Node *)malloc(sizeof(Node));
tmp->value = arr[n - 1];
tmp->next = tmp;
list.head = tmp;
list.tail = tmp;
for (int i = n - 2; i >= 0; i--) {
Node *tmp = (Node *)malloc(sizeof(Node));
tmp->value = arr[i];
tmp->next = list.head;
list.head = tmp;
list.tail->next = tmp;
}
Node *cur = list.tail;
Node *fr;
while (cur->next != cur) {
for (int i = 0; i < m - 1; i++) {
cur = cur->next;
}
ret[count] = cur->next->value;
count++;
fr = cur->next;
cur->next = cur->next->next;
free(fr);
}
ret[count] = cur->value;
free(cur);
}
int main(void) {
int arr[] = {1,2,3,4,5,6,7};
int n = 7;
int m = 3; // from 1
int ret[n];
joseph(arr, n, m, ret);
for (int i = 0; i < n; i++) {
printf("%d\t", ret[i]);
}
return 0;
}
暴力
枚举所有的可能,然后判断哪些符合条件。
算法简单,需要考虑时间复杂度。
求满足条件的最小值
Sn = 1 + 1/2 + 1/3 + … + 1/n。给出一个整数K(1 <= K <= 15),计算最小的 n,使得 Sn > K。
#include <stdio.h>
int min(int k) {
float sum = 0;
for (int i = 1; ; i++) {
sum += 1.0 / i;
if (sum > (float)k) {
return i;
}
}
}
int main(void) {
printf("%d\n", min(1));
printf("%d\n", min(3));
printf("%d\n", min(15));
return 0;
}
百钱百鸡
用一百元买一百只鸡,其中公鸡3元一只,母鸡5元一只,小鸡一元3只。列出所有方案。
对于这个问题,实际上就是求三元一次方程的所有非负整数解。因为只有两个约束条件,所以可能有多个解。条件为:
- x + y + z = 100;
- 3 * x + 5 * y + z / 3 = 100;
- x, y, z 都是非负整数
完全暴力
直接遍历所有可能,复杂度为 O(n^3):
#include <stdio.h>
void j() {
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 100; j++) {
for (int k = 0; k < 100; k++) {
if (i + j + k == 100 &&
k % 3 == 0 &&
3 * i + 5 * j + k / 3 == 100) {
printf("%d, %d, %d\n", i, j, k);
}
}
}
}
}
int main(void) {
j();
return 0;
}
输出:
4, 12, 84
11, 8, 81
18, 4, 78
25, 0, 75
利用已知条件
对于三元一次方程,如果只有一个表达式,则可以把其中的一个变量用另外两个变量来表示,从而只遍历2次。如果有两个表达式,可以把其中的两个变量用另外一个变量表示,从而只遍历1次。
y = (200 - 8 * x) / 14
z = (400 - 2 * x) / 14 * 3
#include <stdio.h>
void j() {
for (int i = 0; i < 100; i++) {
int j = (200 - 8 * i) / 14;
int k = (400 - 2 * i) / 14 * 3;
if (i + j + k == 100 &&
i >= 0 && j >= 0 && k >= 0 &&
k % 3 == 0 &&
3 * i + 5 * j + k / 3 == 100) {
printf("%d, %d, %d\n", i, j, k);
}
}
}
int main(void) {
j();
return 0;
}
求一个整数能不能被0~9组成的两个五位数相除
任意正整数n(2 <= n <= 70),写出所有形式如 abcde/fghij 的除法,其中 a 到 j 刚好是数字 0 到 9,且商等于 n。
这里可以通过递归构造所有满足要求的数字,然后逐个判断是否符合要求。
暴力
暴力枚举每个位置的数字,时间复杂度是 O(10!)。时间复杂度太高,省略代码,可以看下面的暴力加反向乘法。
暴力加反向乘法
对除数进行暴力,然后乘以 n,再判断结果是否满足要求,时间复杂度 O(5!)
package main
import (
"fmt"
"strconv"
)
func main() {
mul(3)
}
func mul(a int) {
var all [][2]int
var ret []int
var noused []int
for j := 0; j < 10; j++ {
noused = append(noused, j)
}
build([]int{}, noused, &ret)
for _, v := range ret {
tmp := a * v
if match(tmp, v) {
all = append(all, [2]int{tmp, v})
}
}
fmt.Println(all)
}
// 判断两个数字是否恰好由0~9这10个数字组成
func match(a int, b int) bool {
var all = make(map[int]bool, 0)
s := strconv.Itoa(a) + strconv.Itoa(b)
for i := 0; i < len(s); i++ {
v := s[i:i+1]
vint, _ := strconv.Atoi(v)
if _, ok := all[vint]; ok {
return false
}
all[vint] = true
}
if _, ok := all[0]; len(s) == 9 && !ok {
all[0] = true
}
if len(all) == 10 {
return true
}
return false
}
// 递归构造所有满足要求的数字
func build(prefix []int, noused []int, ret *[]int) {
if len(prefix) == 5 {
num := 0
for _, v := range prefix {
num = num * 10 + v
}
*ret = append(*ret, num)
return
}
for i, v := range noused {
var noused_new = make([]int, len(noused))
var prefix_new = make([]int, len(prefix))
copy(noused_new, noused)
copy(prefix_new, prefix)
noused_new = append(noused_new[:i], noused_new[i + 1:]...)
prefix_new = append(prefix_new, v)
build(prefix_new, noused_new, ret)
}
}
结果:
[[17469 5823] [17496 5832] [50382 16794] [53082 17694] [61749 20583] [69174 23058] [91746 30582] [96174 32058]]
四则运算
给定一个全是数字的字符串(长5至20),按顺序加入+-*/
四个运算符,求最大的计算结果。有 q 组询问(1 <= q <= 100000)。
例如,对于 12345,其最大结果也是唯一结果,即 1+2-3*4/5 = 1
如果直接暴力,每次大概需要计算 20^4 种情况,乘以 q 组后会超时。分析表达式可知,对于 a+b-c*d/e
,只需要 a b e 尽可能大,c 和 d 尽可能小,结果就是符合要求的。
判断给出的整数数组中能不能取3个组成三角形
三角形三条边的规则:两条短边长度之和一定大于最长边。
给定一个整数数组,最大不超过 1e9,判断其中是否存在可以构成三角形的3个整数。
直接暴力,时间复杂度为 O(n^3)。
如果先对所有元素排序,然后取任意相邻的三个元素,可以发现,如果这3个元素中的两个较小值A B的和都小于较大者C,那么更小的所有元素求和时也会小于较大者C。所以,无法组成三角形的边界条件是:**排序后,对任意相邻3个元素ABC,A+B=C,此时刚好无法构成三角形。**这个条件对应的就是斐波那契数列。对于 1e9,对应斐波那契数列的第 45 个元素,所以一旦给定的数组中的元素大于 45,可以认为一定会构成三角形,否则使用暴力或者排序后判断所有的相邻3个元素是否构成三角形。
看看 1e9 对应斐波那契数列的第几个元素
#include <stdio.h>
int fib(int target) {
int count = 0;
int cur = 0;
int next = 1;
while (cur < target) {
next = next + cur;
cur = next - cur;
count++;
}
return count;
}
int main(void) {
printf("%d\n", fib(1e9));
return 0;
}