1676 字
4 分钟
日常算法随记part.1
2026-03-21
统计加载中...

最近在备赛蓝桥杯,所以刷了点蓝桥杯的题。感觉还是很有意思和收获的,所以写了这篇博客记录了一下自己的一点感悟。

P12131 客流量上限#

题目链接:https://www.luogu.com.cn/problem/P12137
大致题意:有一个数组A1,A2,A3,A4,...,A2025A_1,A_2,A_3,A_4,...,A_{2025},需要满足以下条件:
1.A1,A2,...,A2025A_1,A_2,...,A_{2025}是1到2025的一个排列。
2.对于任意的的iijj (1<=i,j<=20251 <= i,j <= 2025. ii可以等于jj),AiA_iAjA_j 的乘积不得超过i×j+2025i \times j + 2025
求满足条件的分配方案有多少种,输出对109+710^9+7取余之后的结果即可。

代码是很短的,但是思考和推导数学公式的过程还是不容易的。
思路:首先我们可以很容易发现Ai=iA_i = i时一定满足条件。接着我们尝试思考其他可能的解。
我们尝试微扰。只交换相邻两个数的位置,看看是否满足条件。令j=i+1j = i + 1,将原式化简i×(i+1)<=i×(i+1)+2025i \times (i + 1) <= i \times (i + 1) + 2025。显然也是成立的,但我们需要去考虑更加极端的条件。我们让ii也等于jj。那么Ai=i+1A_i = i + 1,这是最极端的情况。Ai2=(i+1)2<=i2+2025A_i^2 = (i + 1)^2 <= i^2 + 2025,从而化简得到i<=1012i <= 1012,也就是说在只交换两个数的情况下已经无法满足所有的数了,那么在更高程度的扰动就更无法满足条件了。
因此,根据结论,所有可能的分配方案有210122^{1012}种。也就是当i<=1012i <= 1012时,有22种选择的可能,A1013A_{1013}的值取决于前面的数哪个没取。但当1014<=i<=20251014 <= i <= 2025时,只能是原来的数。由于数比较大,直接用pow()pow()会超时,所以我们需要用快速幂。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 998244353
const ll MOD = 1e9 + 7;
ll fast_pow(ll a,ll b,ll MOD){
ll ans = 1;
while(b){
if(b & 1) ans = (ans * a) % MOD;
b >>= 1;
a = (a * a) % MOD;
}
return ans;
}
void solved(){
cout << fast_pow(2, 1012, MOD);
}
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solved();
return 0;
}

P12343 树上寻宝#

题目链接:https://www.luogu.com.cn/problem/P12343
大致题意:我们需要在一个节点数为nn的树的根节点上不断向外搜索并累计权值。每次可以操作kk次,每一次都可以走一条边或者两条边。达到kk次之后就会自动收获当前节点的权值并返回根节点,重新进行搜索,求所有能到达的节点的权值之和。
这种题还是挺模板的,多练练就会了。
思路:这里的每次操作一条边或者两条边其实可以理解为搜索的深度x<=2kx <= 2 k的节点权值都可以搜到。而深度大于2k2 k的权值搜不到不计入总和即可。
我们用邻接表来记录一个父节点的相邻节点以遍历。然后用队列来操作父节点搜索,接着将子节点入队作为新父节点,不断向深搜索。(BFSBFS)用depthdepth数组记录对应节点的深度。depthdepth数组初始化为1-1,表示还未搜索和记录深度。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 1000000007LL
vector<vector<int>> g;
vector<ll> w;
queue<int> q;
vector<int> depth;
int n,k;
void solved(){
cin >> n >> k;
w.resize(n);
g.resize(n);
depth.resize(n, -1);
for(auto &it:w){
cin >> it;
}
for (int i = 1; i < n;i++){
int u, v;
cin >> u >> v;
u--, v--;
g[u].push_back(v), g[v].push_back(u);
}
depth[0] = 0;
q.push(0);
while(!q.empty()){
int u = q.front(); q.pop();
for(auto v : g[u]){
if(depth[v] == -1){
depth[v] = depth[u] + 1;
q.push(v);
}
}
}
ll ans = 0;
int limit = k * 2;
for (int i = 0; i < n;i++){
if(depth[i] <= limit){
ans += w[i];
}
}
cout << ans;
}
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solved();
return 0;
}

P12136 生产车间#

题目链接:https://www.luogu.com.cn/problem/P12136
题意:一个有nn个节点的树,节点ii的权值为wiw_i。叶节点的权值代表着会传递wiw_i个产品给父节点,而其他非根节点会向其父节点传递最多wiw_i个产品(也就是说传递的产品数由所有子节点传递的产品数之和决定),现在我们要删去部分节点使得其对应的父节点的权值大于等于所有子节点权值之和,问使得最终传递到根节点的产品数最多是多少。

这题是比较困难的树上dpdp,同时还使用了bitsetbitset压缩状态,蒟蒻博主也是去反复看题解和询问AIAI才最终搞懂。大部分解释都写在注释里了,这里的思路可能主要是补充。顺带一提,注释是自己写的,不是AIAI()。
思路:bitsetbitset:这里的bitssetbitsset可能比较抽象。它其实指的是用位方式权值表示所有可以取到的值。举个例子:010011001010011001,从右往左看,第00位为1,表示的是ii节点的权值 w[i]w[i]可以取到00(也就是不取)。同样地,对应第33位,第44位,第77位表示的是权值w[i]w[i]可以取到3,4,73,4,7。所以我们后续的状态继承也是通过位运算的方式进行。
掩码:掩码设置为不超过权值w[u]w[u]的全11码,这是为了保证继承状态中bitsetbitset的最大位不会超过当前父节点最大权值w[u]w[u]。(超出最大权值的部分会被按位取合操作清零)。
cur._Find_first()cur.\_Find\_first()cur._Find_next()cur.\_Find\_next():前者为从低位开始找,找到第一个为11的位,输出对应的位置。后者为找到从xx位开始找第一个为11的位,然后输出位置。
大佬(不是博主)的AC代码:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 998244353
const int N = 1e3 + 10;
int n,ans;
vector<int> w(N); //权值
vector<vector<int>> g(N); //邻接表
vector<bitset<N>> dp(N); //1,父节点 2,对应的bitset权值
void dfs(int u,int p){
if(g[u].size() == 1 && p != -1){ // 判断是叶节点就返回
dp[u][0] = 1; // 把0位设置为1,表示权值可为0
dp[u][w[u]] = 1; //对应位的设置为1
return;
}
bitset<N> cur,mask; //设置当前的操作权值cur和掩码mask
cur[0] = 1;
for (int i = 0; i <= w[u];i++){ //掩码设置为全1,不超过w[u]
mask.set(i);
}
for(auto v:g[u]){ //遍历对应的邻接表
if(v != p){ //不为父节点继续搜索
dfs(v, u); //搜索到叶节点后开始计算
bitset<N> next;
next[0] = 1;
for (int i = cur._Find_first(); i <= w[u]; i = cur._Find_next(i)){
next |= (dp[v] << i);
}
next &= mask; //掩码保证不超过w[u]
cur = next; //更新cur
}
}
dp[u] = cur; //更新dp[u]
}
void solved(){
cin >> n;
for (int i = 1; i <= n;i++){
cin >> w[i];
}
for (int i = 1; i < n;i++){
int u, v;
cin >> u >> v;
g[u].push_back(v), g[v].push_back(u); //加入邻接图
}
dfs(1, -1); //父节点p设为-1
for (int i = w[1]; i >= 0;i--){ //从高位找到第一位为1的权值即为答案
if(dp[1][i]){
ans = i;
break;
}
}
cout << ans;
}
int main() {
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solved();
return 0;
}

P12137 装修报价#

题目链接:https://www.luogu.com.cn/problem/P12137
题意:NN个数构成的数组A1,A2,...,ANA_1,A_2,...,A_N,在每对相邻的数字中间随意插入 ++、-、\bigoplus三者其中一个(其中在这题中异或的运算优先级最高),计算所有可能的结果的总和并对109+710^9 + 7取余。
这题其实是找数学规律,找到之后用代码实现一下即可。
思路:像这种全排列的求和运算,我们很容易就想到会有一部分的项数会相互抵消掉而不影响最终结果。我们可以看到题目给的对应的例子:只有前三项没有相互抵消掉,而后66项总会出现相同的项数加了又减,前后加减异或的情况。由此我们可以大胆猜想只有前缀异或才会对总和有贡献:每当前kk项全为 \bigoplus时,k+1k+1位的项可以取++,-。而k+2k + 2以及后面所有的项可以任意填++-\bigoplus,并且这些情况是不会被抵消掉的,需要计入总和。因此我们可以得到每个前缀长度为ii的可能结果有 2×3ni12 \times 3^{n - i - 1}种。对应能产出的贡献为pre_xor×2×3ni1pre\_xor \times 2 \times 3^{n-i-1}。(这是因为除了前缀的异或块外后面的项同样会相互抵消,最终真正贡献的是前缀异或的代码块累加了多少次,也就是有多少种)。
这里代码实现前缀异或没有专门开数组计算前缀和,而是用temptemp在每一次循环都计算了该次前缀异或所有的结果并计入总和。对应的情况由于nn最大可以取到10510^5,我们需要使用快速幂。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define endl '\n'
#define mod 1000000007LL
ll fast_pow(ll a,ll b,ll MOD){
ll ans = 1;
while(b){
if(b & 1) ans = (ans * a) % MOD;
b >>= 1;
a = (a * a) % MOD;
}
return ans;
}
void solved(){
ll n;
cin >> n;
vector<ll> a(n + 1);
ll ans = 0;
ll temp = 0;
for (int i = 1; i <= n;i++){
cin >> a[i];
}
for (int i = 1; i <= n;i++){
temp ^= a[i];
if(i < n){
ans += (temp * (2 * fast_pow(3, n - i - 1, mod)) % mod) % mod;
}
else{
ans = (ans + temp) % mod;
}
ans %= mod;
}
cout << ans;
}
int main(){
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solved();
return 0;
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

日常算法随记part.1
https://mkrari.cn/posts/algorithmrecord_1/
作者
Mkrari
发布于
2026-03-21
许可协议
CC BY-NC-SA 4.0