程序设计实践 Week1 解题记录

程序设计实践 Week1 解题记录

Coast23

前言

Week 1 的题目太菜了, 一天就 AK 了.

尽量选了一些相对难的题目来讲.

1. 两数之和

题意简述

给定一个不含重复元素升序数组 a, 求出所有的组合.

题目保证组合唯一.

Solution

不需要双指针, 直接用 unordered_map 记录出现过的数即可.

枚举 a[i], 查找 target - a[i]map 中是否出现过.

时间复杂度. 如果 a 不是有序的, 那么这种做法比双指针优, 因为不需要排序.

AC Code:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <unordered_map>

int a[1000006];
std::unordered_map<int, int> idx;

int target, n;

int main(){
scanf("%d%d", &target, &n);
for(int i = 0; i < n; ++i){
scanf("%d", &a[i]);
idx[a[i]] = i;
}
for(int i = 0; i < n; ++i){
if(idx.find(target - a[i])!= idx.end()){
printf("%d %d\n", i, idx[target - a[i]]);
return 0;
}
}
}

2. 三数之和

题意简述

找出所有数对, 满足:.

不允许有重复数字.

Solution

固定, 然后在中寻找.

为了在线性复杂度内求出, 使用双指针法:

,.

  • , 说明太小, 令.
  • , 说明太大, 令.
  • , 输出即可, 去重后令.

持续以上过程直至.

总的时间复杂度为.

AC Code:

#include <cstdio>
#include <iostream>
#include <algorithm>
#define MAXN 10004

int k, n;
struct val{
int x;
int id;
}a[MAXN];

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch xor 48);
return f ? x : -x;
}

int main(){
k = read(); n = read();
for(int i = 0; i < n; ++i) a[i].x = read(), a[i].id = i;
std::sort(a, a + n, [](val a, val b){return a.x < b.x;});
for(int i = 0; i < n - 2; ++i){
if(i > 0 and a[i].x == a[i-1].x) continue;
int l = i + 1, r = n - 1;
while(l < r){
if(a[i].x + a[l].x + a[r].x > k) r--;
else if(a[i].x + a[l].x + a[r].x < k) l++;
else{
printf("%d %d %d\n", a[i].x, a[l].x, a[r].x);
while(l < r and a[l].x == a[l+1].x) l++;
while(l < r and a[r].x == a[r-1].x) r--;
l++, r--;
}
}
}
}

3. 四数之和

题意简述

找出所有数对, 满足:.

不允许有重复数值.

Solution

固定, 然后用双指针法在中寻找.

时间复杂度.

AC Code:

#include <cstdio>
#include <iostream>
#include <algorithm>
#define MAXN 10004

int k, n;
struct val{
int x;
int id;
}a[MAXN];

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch xor 48);
return f ? x : -x;
}

int main(){
k = read(); n = read();
for(int i = 0; i < n; ++i) a[i].x = read(), a[i].id = i;
std::sort(a, a + n, [](val a, val b){return a.x < b.x;});
for(int i = 0; i < n - 2; ++i){
if(i > 0 and a[i].x == a[i-1].x) continue;
int l = i + 1, r = n - 1;
while(l < r){
if(a[i].x + a[l].x + a[r].x > k) r--;
else if(a[i].x + a[l].x + a[r].x < k) l++;
else{
printf("%d %d %d\n", a[i].x, a[l].x, a[r].x);
while(l < r and a[l].x == a[l+1].x) l++;
while(l < r and a[r].x == a[r-1].x) r--;
l++, r--;
}
}
}
}

4. 汉诺塔I

题意简述

经典问题.

有三根柱子和不同大小的圆盘, 起始时所有圆盘都堆叠在第一根柱子上, 大的在下, 小的在上. 求将所有圆盘移动到第三根柱子上的最少移动步骤.

移动遵循以下规则:

  1. 一次只能移动一个圆盘.
  2. 大盘不能在小盘上面. (目标柱上)
  3. 可以借助中间柱子.
Solution

现对题目进行形式化描述与推导.

: 移动个盘子, 其中为起始柱子,为辅助柱子,为目标柱子.

  • , 则直接移动上的圆盘到上, 也就是打印 A->C.
  • :
    • 先把个盘子从移动到上, 这个时候需要来当辅助柱子, 也就是执行.
    • 然后,上就剩最大的盘子了, 把它移动到上, 打印 A->C.
    • 最后, 把个盘子从移动到上, 这个时候需要来当辅助柱子, 也就是执行.

递归这个 Hanoi 函数, 即可求出最少移动步数.

时间复杂度.

AC Code:

#include <cstdio>
#include <algorithm>

void hanoi(int n, char u, char s, char v){
if(n == 1){
printf("%c->%c\n", u, v);
return;
}
// n-1 from u to v
hanoi(n-1, u, v, s);
// move
printf("%c->%c\n", u, v);
// n-1 from s to v
hanoi(n-1, s, u, v);
}

int main(){
int n;
scanf("%d", &n);
hanoi(n, 'A', 'B', 'C');
}

5. 放苹果

题意简述

求把个苹果放到个盘子的方案数.

.

Solution

表示把个苹果放到个盘子的方案数.

  • , 则(空盘子的方案数为).
  • , 则(没有苹果的方案数为).
  • 特别地, 有(空盘子和空苹果的方案数为).

以上为边界情况, 接下来考虑递推求解.

  • 首先, 如果, 最多只能用到个盘子(各放 1 个苹果), 也就是说 “个苹果放到个盘子” 这个问题等价于 “个苹果放到个盘子”, 所以.

  • 否则, 我们作如下讨论:

    • 个盘子都不能放空. 也就是说, 每个盘子至少要放一个苹果.
      为了保证这一点, 我们可以先在各个盘子上放个苹果, 然后再把剩下的个苹果放到个盘子上.
      方案数为.
    • 个盘子至少有一个为空. 那只要把个苹果放到个盘子上即可.
      方案数为.
      于是, 我们可以得到:.

递推求解即可, 答案为.

时间复杂度.

AC Code:

#include <cstdio>
#include <iostream>

int T, n, m;
long long dp[14][14];

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return f ? x : -x;
}

int main(){
for(int i = 0; i < 14; ++i) dp[0][i] = 1;
for(int i = 0; i < 14; ++i) dp[i][1] = 1;
for(int m = 1; m < 14; ++m){
for(int n = 2; n < 14; ++n){
if(m < n) dp[m][n] = dp[m][m];
else dp[m][n] = dp[m][n-1] + dp[m-n][n];
}
}
T = read();
while(T--){
m = read(); n = read();
printf("%lld\n", dp[m][n]);
}
}

6. 递归求波兰表达式

题意简述

给定一行前缀表达式, 请你计算算式结果.

Solution

虽然题目叫 “递归求“, 但我用了更好看的写法(maybe): .

观察即可得到这样的做法:

从右往左扫描表达式, 遇到数字就压栈, 遇到符号就取出栈顶的两个数字进行运算, 并将运算结果压栈.

最后栈顶元素即为结果.

需要注意, 取出栈顶数字的时候, 先取出来的是左操作数, 后取出来的是右操作数.

可以使用 stod() 函数方便地把字符串转为 double 类型.

AC Code:

#include <stack>
#include <vector>
#include <string>
#include <cstdio>
#include <iostream>

std::vector<std::string> a;
std::stack<double> num;

bool is_opt(const std::string& s) {
return s == "+" || s == "-" || s == "*" || s == "/";
}

int main(){
std::string s;
while(std::cin >> s) a.push_back(s);
for(int i = a.size() - 1; ~i; --i){
if(is_opt(a[i])){
double num1 = num.top(); num.pop(); // 左
double num2 = num.top(); num.pop(); // 右
if(a[i] == "+") num.push(num1 + num2);
if(a[i] == "-") num.push(num1 - num2);
if(a[i] == "*") num.push(num1 * num2);
if(a[i] == "/") num.push(num1 / num2);
}
else num.push(std::stod(a[i]));
}
printf("%.6lf\n", num.top());
}

7. 算式表达式

题意简述

给定一行算式, 运算数在以内, 运算符只有 +*, 求算式对取模的结果.

算式一共有个数字和个运算符,.

Solution

一开始没看数据范围, 想用 Pythoneval() 函数, 结果吃了一发 RE.

其实这题很简单, 只要想到处理 +* 运算优先级的方法就行. 令 ans 表示当前计算的答案, cur 表示当前累计的乘法运算的结果, 从左到右扫描运算符:

  • 对于 *: 将 cur 乘以当前数字.
  • 对于 +: 将 ans 加上 cur, 然后将 cur 置为.

最后输出 (ans + cur) % MOD 即可.

AC Code:

#include <cstdio>
#include <string>
#include <iostream>
#define mod 1000000007

int n;
long long ans = 0;
std::string str;

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch xor 48);
return f ? x : -x;
}

int main(){
n = read(); // useless
std::cin >> str;
long long cur = str[0] xor 48;
for(int i = 1; i < str.length(); i += 2){
char op = str[i];
int num = str[i+1] xor 48;
if(op == '*'){
cur = (cur * num) % mod;
}
else if(op == '+'){
ans = (ans + cur) % mod;
cur = num;
}
}
printf("%lld\n", (ans + cur) % mod);
}

8. 二进制密码锁

题意简述

给定两个 01 字符串, 可对的第位字符进行操作, 操作会改变及其相邻字符的状态, 求将变为的最少操作次数.

Solution

按位异或得到, 目标即为的每一位都变成.

我们从左到右扫描, 如果, 那么就需要靠进行翻转来将.

在这个策略下, 对于, 它可以靠自身翻转来改变状态, 也可以靠进行翻转来改变状态.

怎么办呢? 那就对是否翻转进行分类讨论, 分别求出需要翻转的次数, 取最优即可.

AC Code:

#include <cstdio>
#include <cstring>
#include <iostream>

int n, ans;
char in[35];
int a[35];
int b[35];

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return f ? x : -x;
}

void flip(int i, int n, int arr[]){ // 对 arr[i] 进行操作
arr[i] = !arr[i];
if(i > 1) arr[i-1] = !arr[i-1];
if(i < n) arr[i+1] = !arr[i+1];
}

int min(int a, int b){return a < b? a : b;}

int main(){
std::cin >> in;
int n = strlen(in);
for(int i = 1; i <= n; ++i) a[i] = in[i-1] xor 48;
std::cin >> in;
for(int i = 1; i <= n; ++i) a[i] ^= (in[i-1] xor 48), b[i] = a[i];

int ans1 = 1, ans2 = 0;
// 翻转第1个
flip(1, n, a);
for(int i = 2; i <= n; ++i) if(a[i-1]) flip(i, n, a), ++ans1;
// 不翻转第1个
for(int i = 2; i <= n; ++i) if(b[i-1]) flip(i, n, b), ++ans2;

if(a[n]) ans1 = 11451419;
if(b[n]) ans2 = 11451419;
ans = min(ans1, ans2);
ans == 11451419? puts("impossible") : printf("%d\n", ans);
}

9. 熄灯问题

题意简述

给定一个的 01 矩阵, 可对进行操作, 操作会改变及其相邻元素的状态, 要求最小化矩阵变为的操作次数.

输出字典序最小的对的操作方案 (记为) .

其实也就是二维的 “二进制密码锁”.

Solution

显然,的操作次数.

显然, 若的状态确定, 那么也就确定了, 因为现在只有能影响的状态.

因此, 只需要枚举, 即经过操作后第一行的状态, 就可以依次确定~的状态.

最后判断是否已经变为, 若是, 输出即可.

时间复杂度 O(), 这里为常数, 直接说好像也没啥问题.

AC Code:

// 太好了只有 6 * 6, 不需要考虑状压
#include <cstdio>
#include <cstring>
#include <iostream>

int T;
int a[10][10];
int res[10][10];

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch xor 48);
return f? x : -x;
}

void rev(int x, int y){
a[x][y] = !a[x][y];
if(x > 1) a[x-1][y] = !a[x-1][y];
if(x < 5) a[x+1][y] = !a[x+1][y];
if(y > 1) a[x][y-1] = !a[x][y-1];
if(y < 6) a[x][y+1] = !a[x][y+1];
}

bool test(){
int b[10][10];
for(int i = 1; i <= 5; ++i) for(int j = 1; j <= 6; ++j) b[i][j] = a[i][j];

for(int i = 1; i <= 6; ++i){
if(res[1][i]) rev(1, i);
}
for(int i = 2; i <= 5; ++i){
for(int j = 1; j <= 6; ++j){
if(a[i-1][j]) res[i][j] = 1, rev(i, j);
}
}
bool flag = true;
for(int i = 1; i <= 5; ++i){
for(int j = 1; j <= 6; ++j){
if(a[i][j]){flag = false; break;}
}
}
if(!flag){
for(int i = 1; i <= 5; ++i) for(int j = 1; j <= 6; ++j) a[i][j] = b[i][j];
return false;
}
for(int i = 1; i <= 5; ++i){
for(int j = 1; j <= 6; ++j){
printf("%d ", res[i][j]);
}
putchar('\n');
}
return true;
}

void solve(){
for(int i = 1; i <= 5; ++i){
for(int j = 1; j <= 6; ++j) a[i][j] = read();
}
for(int mask = 0; mask < (1 << 6); ++mask){
int m = mask;
memset(res, 0, sizeof(res));
for(int i = 1; i <= 6; ++i) res[1][i] = m & 1, m >>= 1;
if(test()) return;
}
}

int main(){
// freopen("input.txt", "r", stdin);
T = read();
for(int i = 1; i <= T; ++i){
printf("PUZZLE #%d\n", i);
solve();
}
}

10. 拨钟问题

题意简述

种拨钟方式, 有个钟, 求将所有钟都拨到点的最少次数的拨钟方案.

Solution

for 循环枚举所有状态还是有点非人类了, 我选择 BFS.

思路很简单, 开一个结构体 Status 表示状态, 其中 c 存当前的钟的状态, step 存初态到这个状态的操作方案, correct 函数用于判断是否所有钟都到 12 点了.

另外, 为了防止重复搜索, 需要一个 vis 数组记录状态是否被搜索过.

再写一个简单的 to_int 函数用于把钟的状态映射为唯一的 int 数即可. 最简单的方式就是把 c 看成一个四进制数.

然后就没啥好说的了.

时间复杂度.

AC Code:

#include <queue>
#include <vector>
#include <cstdio>
#include <iostream>

char move[10][6] = {
"",
"ABDE",
"ABC",
"BCEF",
"ADG",
"BDEFH",
"CFI",
"DEGH",
"GHI",
"EFHI"
}; // 照抄题目的拨钟方式, 不然容易错, 错了很难debug

bool vis[1145141];

struct Status{
std::vector<int> c; // 0-4 表示各个钟的状态;
std::vector<int> step; // 记录操作步骤
bool correct(){ // 判断是否所有钟都到 12 点了
bool flag = true;
for(int i = 0; i < 9; ++i) if(c[i] % 4 != 0){flag = false; break;}
return flag;
}
};

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return f ? x : -x;
}

int to_int(std::vector<int>& c){ // 映射为唯一的 int 数, 把钟看成 4 进制数即可.
int res = 0;
for(int i = 0; i < 9; ++i) res = (res << 2) + c[i];
return res;
}

void bfs(){
std::queue<Status> q;
Status s;
for(int i = 0; i < 9; ++i) s.c.push_back(read());
q.push(s); // 初状
while(q.size()){
Status s = q.front(); q.pop();
if(s.correct()){ // 找到了, 直接输出即可, BFS 的层序遍历方式可保证这个就是最优解.
for(int i = 0; i < s.step.size(); ++i) printf("%d ", s.step[i]);
return;
}
for(int i = 1; i <= 9; ++i){
Status t = s;
for(int j = 0; move[i][j]; ++j){
t.c[move[i][j] - 'A'] = (t.c[move[i][j] - 'A'] + 1) % 4; // 解析题目给的阴间拨钟方式
}
int t_hash = to_int(t.c);
if(!vis[t_hash]){ // 只有未搜过的状态才放进队列, 不然会爆炸
vis[t_hash] = true;
t.step.push_back(i);
q.push(t);
}
}
}
}

int main(){
bfs();
}

11. 求排列的逆序数

题意简述

给定一个长为的序列, 求的逆序对数.

逆序对定义为.

Solution

众所周知求逆序对可以用归并排序或者树状数组.

但我已经忘记树状数组怎么写了, 所以用的归并排序.

归并排序时, 我们能直接根据要插入的元素的位置计算出它的逆序对数, 累加这个答案进计数器即可.

归并排序没啥好说的, 计算方式见代码注释.

AC Code:

#include <bits/stdc++.h>
#define int long long
#define MAXN 500005

int n, inverse_cnt;
int a[MAXN], b[MAXN];

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch xor 48);
return f ? x : -x;
}

void merge_sort(int l, int r){
if(l >= r) return;
int mid = (l + r) >> 1;
merge_sort(l, mid); merge_sort(mid + 1, r);
int i = l, j = mid + 1, tot = 0;
while(i <= mid and j <= r){
if(a[j] < a[i]){
b[++tot] = a[j++];
inverse_cnt += mid - i + 1;
/*
注意, 这里 i < j 且 a[i] > a[j], 会产生逆序对.
归并排序的特征是, [l, mid] 与 [mid+1, r] 都已经分别升序.
既然 a[j] 比 a[i] 小, 那它肯定比 [i, mid] 中的所有元素都小, 因此它贡献了 (mid - i + 1) 个逆序对.
*/
}
else b[++tot] = a[i++]; // 这里 i < j 且 a[i] <= a[j], 不会产生逆序对.
}
while(i <= mid) b[++tot] = a[i++];
while(j <= r) b[++tot] = a[j++];
for(int k = l; k <= r; ++k) a[k] = b[k - l + 1];
}

signed main(){
n = read();
for(int i = 1; i <= n; ++i) a[i] = read();
merge_sort(1, n);
printf("%lld\n", inverse_cnt);
return 0;
}

12. 最小预算值

题意简述

给定长为的序列和正整数, 要求至多把分为 连续的段, 最小化这些连续子段和的最大值.

Solution

“最小化最大值”, 经典的二分答案转判定.

有两个点需要注意:

  • 二分时, 初始边界不应设为, 而应是.
  • 二分有多种写法, 要注意边界的判定和答案的选择, 这里推荐用另一个计数器来记录可行的答案, 收缩区间时把端点都排除, 这样就不用纠结答案是落在还是了.

AC Code:

#include <cstdio>
#include <iostream>

int n, m;
int a[200005];
long long ans;

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch ^ 48);
return f ? x : -x;
}

bool check(const long long x){
// 贪心判断以 x 作为最大子段和进行划分, 最多能分几段.
// 如果段数不超过 m, 说明 x 这个子段和是可行的.
int seg = 1; // 注意, seg 从 1 开始计算.
long long cnt = a[1];
for(int i = 2; i <= n; ++i){
if(cnt + a[i] > x){
cnt = a[i];
++seg;
}
else cnt += a[i];
}
return seg <= m;
}

int main(){
n = read(), m = read();
long long l = -1145141919810LL, r = 0;
for(int i = 1; i <= n; ++i) a[i] = read(), r += a[i], l = l > a[i] ? l : a[i];
while(l <= r){
long long mid = (l + r) >> 1;
if(check(mid)) r = mid - 1, ans = mid; // ans 记录了最小的可行的 mid.
else l = mid + 1; // 边界收缩, 排除端点
}
printf("%lld\n", ans);
// 以下是另一种写法, 这种写法的思路是让答案落在区间 (l, r], 最后答案即为 r.
/* while(l < r){
long long mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
printf("%lld\n", r);
*/
}

13. 林克的蛋糕

题意简述

给定个物品的体积, 和整数, 可以把每个物品分为体积为的若干份, 要求份数和大于, 请最大化.

与上一题的不同之处在于,是浮点数, 答案保留小数点后位.

Solution

浮点数二分, 和整数二分的思路一样, 只需要改一下二分条件即可.

整数二分条件一般是 l < rl <= r, 浮点数二分改成 r - l > eps, 其中 eps 是自定义的精度.

AC Code:

#include <cstdio>
#include <iostream>
#define exp 1e-5
#define PI 3.1415926535898

int n, f;
int a[20004];

int read(){
int x = 0, f = 1; char ch = getchar();
for(; !isdigit(ch); ch = getchar()) f ^= (ch == '-');
for(; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + (ch xor 48);
return f? x : -x;
}

bool check(double x){
int cnt = 0;
for(int i = 1; i <= n; ++i) cnt += int(a[i] * a[i] * PI / x);
return cnt > f;
}

int main(){
n = read(), f = read();
double l = exp, r = 0, ans;
for(int i = 1; i <= n; ++i) a[i] = read(), r += PI * a[i] * a[i];
while(r - l > exp){
double mid = (l + r) / 2;
if(check(mid)){
ans = mid;
l = mid;
}
else r = mid;
}
printf("%.3lf\n", ans);
}
  • 标题: 程序设计实践 Week1 解题记录
  • 作者: Coast23
  • 创建于 : 2025-06-30 09:19:13
  • 更新于 : 2025-07-03 14:16:46
  • 链接: https://coast23.github.io/2025/06/30/程序设计实践-Week1-解题记录/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论