第二次训练C、D、M、X题解

本文分享了作者的C++编程习惯,介绍了万能头文件、预编译宏的使用,并通过具体题目详解了矩阵乘法、快速幂及矩阵快速幂算法的应用。

#本人代码习惯
第一次写博客简单说一下个人的代码习惯以及介绍一些小东西

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

万能头文件大多数OJ和比赛环境都支持这个头文件 相当于一次性引用了所有头文件
C++标准库中 基本所有函数都需要在前面带上std:: 可以直接使用using namespace std;启用std命名空间就不用再打std::了

#include <stdio.h>

在题目有大量数据输入输出时C++的接受速度会比C标准库中的慢很多(cstdio中的scanf也比stdio.h中的慢) 所以习惯性就第一行带上stdio.h的头文件

const int INF = 0x3f3f3f3f;

竞赛常用int最大值
好处memset(arr, 0x3f, sizeof(arr));可以直接将整个数组初始化为这个值
还有这个值相加不爆int相乘不爆long long

const int MAXN = 1e2 + 10;

定义MAXN方便整体调整数组大小 开头定义后面统一使用注意要加const才可以

#ifdef LOCAL
#endif

其中#ifdef LOCAL与#endif是预编译宏
当定义了#define LOCAL或在编译器中附加指令(拿DevC++5.4.0举例子在工具选项中加入-DLOCAL)
只有这样#ifdef LOCAL与#endif之间的代码才会在编译后生效
用处就是可以在中间加上调试指令 提交到OJ的时候并不会生效而影响最终答案
这里写图片描述

freopen("C:/input.txt", "r", stdin);

重定向输入流 从文件读入数据
在做题需要调试的时候 如果题目数据量比较大每次都输入过于麻烦 可以使用这条指令让程序从文件读入(C:/input.txt文件也可以是相对路径)

#C题 矩阵乘法 <模拟>
按照数学的矩阵乘法规则进行模拟(详见矩阵乘法_百度百科)
附上AC代码

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

const int INF = 0x3f3f3f3f;
const int MAXN = 1e2 + 10;
int g1[MAXN][MAXN], g2[MAXN][MAXN], g[MAXN][MAXN];

int main()
{
#ifdef LOCAL
	freopen("C:/input.txt", "r", stdin);
#endif
	int n;
	cin >> n;
	for (int i = 0; i < n; i++) //接受矩阵1存为g1
		for (int j = 0; j < n; j++)
			scanf("%d", &g1[i][j]);
	for (int i = 0; i < n; i++) //接受矩阵2存为g2
		for (int j = 0; j < n; j++)
			scanf("%d", &g2[i][j]);
	for (int i = 0; i < n; i++) //矩阵相加存为g
		for (int j = 0; j < n; j++)
			for (int k = 0; k < n; k++)
				g[i][j] += g1[i][k] * g2[k][j];
	for (int i = 0; i < n; i++) //输出g
	{
		for (int j = 0; j < n; j++)
		{
			if (j)
				putchar(' '); //注意行尾不能多空格
			printf("%d", g[i][j]);
		}
		putchar('\n'); //每行有换行符
	}

	return 0;
}

#D题 WERTYU <字符串>
题目字符种类和字符串总长度都很小 直接暴力做法
先建立一个字符串列表mp每个字符都进行查找然后输出其前一位的字符
采用修改接收到的字符数组再输出的方法也行 注意多样例之间要输出换行否则会答案错误

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

const int INF = 0x3f3f3f3f;

int main()
{
#ifdef LOCAL
	freopen("C:/input.txt", "r", stdin);
#endif
	char mp[] = "`1234567890-=QWERTYUIOP[]\ASDFGHJKL;'ZXCVBNM,./"; //滚键盘写的字符列表
	char c;
	while (scanf("%c", &c) != EOF) //我采用每次接受一个字符然后输出
	{
		int flag = 1;
		for (int i = 0; mp[i] != 0; i++)
		{
			if (mp[i] == c)
			{
				printf("%c", mp[i - 1]);
				flag = 0;
				break;
			}
		}
		if (flag) //如果接收到的不是字符列表中的字符则直接输出 如果输出过了就不会再输出了(换行符)
			printf("%c", c);
	}

	return 0;
}

#M题 A^B Mod C <数论> <快速幂>
数论的快速幂算法 题目给出A,B,C都是在1e9范围内的 如果采用朴素算法一次一次往上乘会超时
快速幂算法的思想 a^11 = a^8 * a^2 * a^1将次方转化为二进制的和然后用一个变量代表着a的二进制次方每次进行平方对应B次方的二进制位乘入答案中并且取模
百度百科_快速幂
为什么数据要取模

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

const int INF = 0x3f3f3f3f;

int main()
{
#ifdef LOCAL
	//freopen("C:/input.txt", "r", stdin);
#endif
	long long a, b, c, ans = 1; //直接定义为long long后面不用担心相乘的时候还没取模就爆了int
	cin >> a >> b >> c;
	while (b) //b为次方数量 此处的意思为b为0的时候不在进行循环
	{
		if (b & 1) //如果b的二进制位末尾为1则将a的对应次方乘入最终答案 详见百度百科~
			ans = (ans * a) % c; //乘完了记得取模
		a = (a * a) % c, b >>= 1; //a自身平方 相当于a^4->a^8或a^8->a^16等 b >>= 1为C++中的位运算操作 相比/2会更快
	}
	cout << ans << endl;

	return 0;
}

#X题 斐波那契数列的第N项 <数论> <矩阵快速幂>
前面的矩阵乘法和快速幂就是为这题铺垫的
矩阵快速幂 利用矩阵乘法得到递推式的某一项 矩阵的乘法过程中利用快速幂加快计算
假设A矩阵为
[ F [ i − 1 ] F [ i ] ] \left[ \begin{matrix} F[i - 1] &amp; F[i] \end{matrix} \right] [F[i1]F[i]]
B矩阵为
[ 0 1 1 1 ] \left[ \begin{matrix} 0 &amp; 1\\ 1 &amp; 1 \end{matrix} \right] [0111]
A矩阵乘上B矩阵之后就会得到
[ F [ i ] F [ i − 1 ] + F [ i ] ] \left[ \begin{matrix} F[i] &amp; F[i - 1] + F[i] \end{matrix} \right] [F[i]F[i1]+F[i]]
根据公式F(n) = F(n - 1) + F(n - 2)得
[ F [ i ] F [ i + 1 ] ] \left[ \begin{matrix} F[i] &amp; F[i + 1] \end{matrix} \right] [F[i]F[i+1]]
等于A矩阵的两项都往后移了一项 乘的这个B矩阵是F(n) = F(n - 1) + F(n - 2)递推式的转移矩阵

如果A矩阵为
[ F [ 0 ] F [ 1 ] ] \left[ \begin{matrix} F[0] &amp; F[1] \end{matrix} \right] [F[0]F[1]]
乘上转移矩阵的n-1次方后就会转化为
[ F [ n ] F [ n + 1 ] ] \left[ \begin{matrix} F[n] &amp; F[n + 1] \end{matrix} \right] [F[n]F[n+1]]
这个矩阵的 左边的一项则为所求 下面的代码中省去了A矩阵 直接求B矩阵的n-1次方
其中的u最开始的状态为单位矩阵 相当于在快速幂中最开始的ans = 1 任何矩阵乘上单位矩阵都等于原矩阵

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

const int INF = 0x3f3f3f3f;
const int MOD = 1e9 + 9;
const int MAXN = 2; //强迫症 就算是2我也要写MAXN

void matrix(long long a[MAXN][MAXN], const long long b[MAXN][MAXN]) //矩阵乘法函数 将ab矩阵相乘然后保存回a矩阵
{
	long long t[MAXN][MAXN]; //定义一个矩阵用来保存相乘的结果2*2的二位可以开在全局变量中 太大会崩溃
	memset(t, 0, sizeof(t));
	for (int i = 0; i < MAXN; i++)
		for (int j = 0; j < MAXN; j++)
			for (int k = 0; k < MAXN; k++)
				t[i][j] = (t[i][j] + a[i][k] * b[k][j]) % MOD; //矩阵相乘
	for (int i = 0; i < MAXN; i++)
		for (int j = 0; j < MAXN; j++) 
			a[i][j] = t[i][j]; //覆盖存入a中
}
int main()
{
#ifdef LOCAL
	//freopen("C:/input.txt", "r", stdin);
#endif
	long long n;
	cin >> n;
	if (n == 0) //0和1的时候直接特判进行输出
		cout << 0 << endl;
	if (n == 1)
		cout << 1 << endl;
	long long t[MAXN][MAXN] = { 1, 1, 1, 0 }, u[MAXN][MAXN] = { 1, 0, 0, 1 }; //转移矩阵t 单位矩阵u
	n -= 1; //这里n不减少一次会多计算一项
	while (n) //矩阵快速幂 将矩阵的n次方转换为快速幂的形式
	{
		if (n & 1)
			matrix(u, t);
		matrix(t, t);
		n >>= 1;
	}
	cout << u[0][0] << endl; //输出u[0][0]即为第n项的答案

	return 0;
}
基于STM32 F4的永磁同步电机无位置传感器控制策略研究内容概要:本文围绕基于STM32 F4的永磁同步电机(PMSM)无位置传感器控制策略展开研究,重点探讨在不依赖物理位置传感器的情况下,如何通过算法实现对电机转子位置和速度的精确估计与控制。文中结合嵌入式开发平台STM32 F4,采用如滑模观测器、扩展卡尔曼滤波或高频注入法等先进观测技术,实现对电机反电动势或磁链的估算,进而完成无传感器矢量控制(FOC)。同时,研究涵盖系统建模、控制算法设计、仿真验证(可能使用Simulink)以及在STM32硬件平台上的代码实现与调试,旨在提高电机控制系统的可靠性、降低成本并增强环境适应性。; 适合人群:具备一定电力电子、自动控制理论基础和嵌入式开发经验的电气工程、自动化及相关专业的研究生、科研人员及从事电机驱动开发的工程师。; 使用场景及目标:①掌握永磁同步电机无位置传感器控制的核心原理与实现方法;②学习如何在STM32平台上进行电机控制算法的移植与优化;③为开发高性能、低成本的电机驱动系统提供技术参考与实践指导。; 阅读建议:建议读者结合文中提到的控制理论、仿真模型与实际代码实现进行系统学习,有条件者应在实验平台上进行验证,重点关注观测器设计、参数整定及系统稳定性分析等关键环节。
题解 **Code Jam 2013 - 第1轮A组** **分析:好运** **运气因素** 这个问题不同寻常,因为你没有全部信息,必须在解题过程中做出猜测。我们觉得这相比通常的确定性问题来说是一种有趣的改变。 然而,运气在这个问题中并没有起到太大作用。第一组数据足够简单,许多方法都能奏效。第二组数据更难一些,但我们估计最优解成功的概率非常高:最优解失败的概率大约只有百万分之一!这是因为8000次独立猜测的数量很大,而限制值X设置得相当保守——大约比正确猜测的期望数量低5个标准差。 **最优策略** 人们可能会倾向于使用各种启发式方法来推断隐藏数字可能是什么。但在这种情况下,最好以科学的方式处理问题,始终追求成功率最高的选择! 为此,我们计算每种可能情况(对于大数据集共18564种)的概率,并选择概率最大的那个。 为什么是18564?每个隐藏数字有7种选择,总共12个隐藏数字,看似会有 $7^{12} = 13841287201$ 种可能性,数量巨大。但由于隐藏数字的顺序无关紧要,实际不同的组合数为 $\binom{12 + 7 - 1}{7 - 1} = 18564$。尝试推导这个公式!或者直接生成所有组合并计数即可。 **先验概率:当 K=0 时** 如果 K=0,意味着我们对隐藏数字毫无信息?也许你会认为此时猜什么都一样,因为所有可能性看起来等概率。很多参赛者犯了这个错误。实际上,即使没有额外信息,某些组合本身就比其他组合更可能出现! 例如,在小数据集中:333 比 234 更少见,确切地说要少6倍。为什么?因为234可以由6种不同排列(234, 243, 324, 342, 423, 432)产生,而333只能由一种方式生成。 一般地,若数字 $d$ 在隐藏卡片中出现 $C_d$ 次,则该集合的概率为: $$ \frac{N!}{C_2! \times \cdots \times C_M! \times (M-1)^N} $$ **K=1 与贝叶斯定理** 我们已经计算出每组隐藏卡片的先验概率,但尚未利用关键信息:K个随机子集的乘积。如何使用这些信息?条件概率正是解决此类问题的工具。 从 K=1 开始。对于每一组隐藏数字A,我们已知其发生的概率 $Pr(A)$。我们也知道某个随机子集的乘积 $p$。我们要计算的是:在观察到子集乘积为 $p$ 的条件下,隐藏集合为A的条件概率,记作 $Pr(A | p)$。 如何计算?使用条件概率定义: $$ Pr(A | p) = \frac{Pr(A \cap p)}{Pr(p)} = \frac{Pr(A) \cdot Pr(p | A)}{Pr(p)} $$ 这一推导称为**贝叶斯定理**。 我们已经知道 $Pr(A)$,因此只需计算 $Pr(p | A)$。我们可以预先为每个A计算这些值:遍历每个可能隐藏集合的所有子集,记录每种子集对应的乘积,并建立一个包含所有这些概率的大表。共有约 $18564 \times 2^{12} \approx 7600$ 万种子集。 然后,$Pr(p)$ 可通过对所有A求和得到:$\sum_A Pr(A) \cdot Pr(p | A)$。 **完整解法** K大于1也没关系:我们将上述推理过程对每一个乘积重复执行,在过程中不断更新隐藏组合的概率分布。 完整的解决方案如下: 1. **预处理阶段**: - 生成所有可能的隐藏数字组合(忽略顺序)。 - 计算每个隐藏集合的初始概率。 - 对每个可能的隐藏集合,枚举其所有子集,计算各子集乘积,并统计 $Pr(p | A)$。按乘积 $p$ 建立索引以便后续快速查找。 2. **主循环阶段**: - 对每个测试用例:从预计算的初始概率分布开始。 - 逐个读取一个乘积 $p$,利用贝叶斯定理和预计算的条件概率 $Pr(p | A)$ 更新当前概率分布。 3. **输出结果**: - 输出当前概率最高的隐藏集合作为答案。
01-01
感谢你提供的详细题解!这确实是一个非常有趣的概率推理问题,不同于传统的确定性算法题。我们来**系统化地实现这个“贝叶斯最优策略”**,并基于题解中提到的组合数优化、先验概率计算和贝叶斯更新机制,给出一个高效的 C++14 实现。 --- ## ✅ 目标:实现贝叶斯最大后验估计(MAP)解法 ### 🧩 核心思想 - 枚举所有可能的 **multiset 组合**(顺序无关) - 计算每个组合的 **先验概率** $ P(A) $ - 预计算每个组合能生成的子集乘积分布 $ P(p|A) $ - 对每个测试用例,使用贝叶斯定理逐个更新后验概率: $$ P(A | p_1,\dots,p_K) \propto P(A) \prod_{i=1}^K P(p_i | A) $$ - 输出最大后验概率的组合 --- ## 🔢 关键公式回顾 ### 1. 先验概率(考虑排列数) 若隐藏数字为 multiset $A$,其中数字 $d$ 出现 $C_d$ 次,则其出现的概率为: $$ P(A) = \frac{N!}{\prod_{d=2}^M C_d! \cdot (M-1)^N} $$ 因为原始选择是独立均匀采样,所以不同排列都算作不同路径,但最终集合相同。 > 注意:共有 $(M-1)^N$ 种等可能的序列(从 $2$ 到 $M$),而某个 multiset 的排列数是 $\frac{N!}{\prod C_d!}$ ### 2. 条件概率 $P(p|A)$ 对给定的组合 $A$,枚举所有 $2^N$ 个子集,计算乘积,统计频率即可。 由于每个元素被选中的概率是 0.5 且独立,每个子集出现概率相等 → 所以 $P(p|A) = \frac{\text{生成 }p\text{ 的子集数量}}{2^N}$ --- ## ✅ C++14 实现(适用于两个数据集) 我们将重点处理第二个数据集:$N=12, M=8$ 虽然总序列有 $7^{12}$,但我们只枚举 **非递减的组合(即 multiset)**,共 $\binom{12+7-1}{7-1} = \binom{18}{6} = 18564$ 种。 ```cpp #include <iostream> #include <vector> #include <map> #include <algorithm> #include <cmath> #include <iomanip> using namespace std; typedef long long ll; // 全局参数 int N, M, K; vector<vector<int>> all_combinations; // 所有可能的 non-decreasing 组合 vector<double> prior_probs; // 每个组合的先验概率 vector<map<ll, double>> likelihood_tables; // likelihood_tables[i][p] = P(p | A_i) // 工具函数:计算阶乘 vector<ll> fact; void precompute_factorials(int max_n) { fact.resize(max_n + 1); fact[0] = 1; for (int i = 1; i <= max_n; ++i) { fact[i] = fact[i - 1] * i; } } // 生成所有 non-decreasing 的 N 元组(值域 [2, M]) void generate_combinations(vector<int>& current) { if (current.size() == N) { all_combinations.push_back(current); return; } int start = current.empty() ? 2 : current.back(); // 非递减排列 for (int x = start; x <= M; ++x) { current.push_back(x); generate_combinations(current); current.pop_back(); } } // 计算组合的先验概率 double compute_prior(const vector<int>& combo) { map<int, int> count; for (int x : combo) count[x]++; ll perm_count = fact[N]; for (auto& kv : count) { perm_count /= fact[kv.second]; } double total_sequences = pow(M - 1, N); // (M-1)^N return perm_count / total_sequences; } // 生成一个组合的所有子集乘积,并构建条件概率表 map<ll, double> build_likelihood_table(const vector<int>& combo) { map<ll, int> freq; int n = combo.size(); for (int mask = 0; mask < (1 << n); ++mask) { ll prod = 1; for (int i = 0; i < n; ++i) { if (mask & (1 << i)) { prod *= combo[i]; } } freq[prod]++; } map<ll, double> table; int total = (1 << n); for (auto& kv : freq) { table[kv.first] = (double)kv.second / total; } return table; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); precompute_factorials(20); // 足够大 int T, R; cin >> T >> R >> N >> M >> K; // 步骤1: 生成所有 non-decreasing 组合 vector<int> cur; generate_combinations(cur); cerr << "Total combinations: " << all_combinations.size() << endl; // 步骤2: 计算先验概率 prior_probs.resize(all_combinations.size()); likelihood_tables.resize(all_combinations.size()); for (int i = 0; i < all_combinations.size(); ++i) { prior_probs[i] = compute_prior(all_combinations[i]); likelihood_tables[i] = build_likelihood_table(all_combinations[i]); } cout << "Case #1:" << endl; // 处理每组测试数据 for (int r = 0; r < R; ++r) { vector<ll> products(K); for (int i = 0; i < K; ++i) { cin >> products[i]; } // 初始化后验概率(使用对数避免下溢) vector<double> log_posterior(all_combinations.size()); for (int i = 0; i < all_combinations.size(); ++i) { log_posterior[i] = log(prior_probs[i]); } // 贝叶斯更新:对每个观测乘积 for (ll p : products) { for (int i = 0; i < all_combinations.size(); ++i) { auto it = likelihood_tables[i].find(p); double prob_p_given_A = (it != likelihood_tables[i].end()) ? it->second : 1e-30; log_posterior[i] += log(prob_p_given_A); } } // 找出最大后验概率的索引 int best_idx = 0; for (int i = 1; i < all_combinations.size(); ++i) { if (log_posterior[i] > log_posterior[best_idx]) { best_idx = i; } } // 输出最佳猜测(顺序任意,直接输出组合) for (int x : all_combinations[best_idx]) { cout << x; } cout << '\n'; } return 0; } ``` --- ## 🔍 代码详解 | 模块 | 功能 | |------|------| | `generate_combinations` | 生成所有 non-decreasing 序列(代表 multiset) | | `compute_prior` | 使用排列数公式计算该 multiset 的先验概率 | | `build_likelihood_table` | 枚举所有子集乘积,统计频率作为 $P(p|A)$ | | 主循环 | 使用对数空间进行贝叶斯更新,防止浮点下溢 | | 输出 | 输出最可能的组合 | --- ## ⚙️ 性能分析 - 组合总数:$\binom{12+7-1}{6} = 18564$ - 每个组合需枚举 $2^{12} = 4096$ 个子集 → 总操作约 $18564 \times 4096 \approx 76$ million,在现代 CPU 上预处理只需几秒 - 每组输入需遍历 18564 × 12 次查找 → 单组约 0.1 秒,8000 组 ≈ 800 秒?太慢! ⚠️ 但是注意:**R=8000 是总共的测试用例数,不是要实时运行 8000 次训练过程!** 实际上,预处理只需要做一次,之后每个测试用例只需执行贝叶斯更新(18564 × K 次 log 加法)。在 OJ 上通常允许几十秒运行时间。 > 提示:可将预处理结果写成静态表硬编码进程序以加速提交(适用于固定参数) --- ## ✅ 为什么这个方法几乎必胜? 如题解所述: - 最优策略的成功率极高 - 错误猜测的标准差很小(大数定律) - X=1120 设置得很宽松(远低于期望正确数) 因此,只要实现正确,大概率能猜对超过 1120 组。 --- ## 💡 改进建议 1. **对数空间稳定计算** - 我们已经用了 `log`,防止数值下溢 2. **剪枝低概率组合** - 在更新过程中动态丢弃概率极低的组合 3. **缓存高频乘积** - 建立全局映射:`product -> list of candidate indices` 4. **多线程预处理** - 可并行化 `likelihood_tables` 的构建 5. **编译期常量表** - 若参数固定(如 N=12,M=8),可把 `all_combinations` 和 `prior_probs` 存为数组,避免重复生成 --- ## ✅ 示例输入输出(小数据集验证) 输入: ``` 1 2 3 4 4 9 4 36 1 1 1 1 1 ``` 输出可能为: ``` Case #1: 334 222 ``` > 注:实际输出取决于哪个组合具有最高后验概率 --- ### ❓相关问题
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值