做题记录
CF2008 G. Sakurako's Task
纪念第一次场上切的一道 G 题,想了 \(40\) 多分钟。
题意
给定数组 \(a\),可以进行任意次操作:选定 \(i,j\), 可以操作 \(a_i \leftarrow a_i+a_j\) 或 \(a_i \leftarrow a_i-a_j\)。
问 数组 a 中不存在的第 \(k\) 个非负整数的最大可能为多少。
思路
首先发现这个第 \(k\) 个非负整数比较奇怪,不太好入手。但是可以发现尽量把数往小了堆是更优的。
转变思路从操作入手,发现如果我们可以获得一个很小的数 \(d\),且其他数都是 \(d\) 的倍数,那么我们可以先把所有数都变成 \(0\),只剩下一个 \(d\),然后可以将数组构造成 \(0,d,2d,3d \dots (n-1)d\),保证数都是最小的。
那么思考如何得到最小的 \(d\),对于两个数 a,b ,我们可以用类似辗转相减的方法得到 \(gcd(a,b)\),同理对于 \(a_i\),我们可以得到最小的 \(d = gcd(a_1,a_2,\dots a_n)\),于是本题就做完了。
Artoj P3183. 游戏升级
模拟赛的好题。
题目大意
t 组询问,每次给定 \(a_1,a_2,c,n\),求:
整除分块很一眼,然后想法是先对 \(a_1\) 做一遍用哈希记录,然后做 \(a_2\) 时添加答案。
但是 哈希会超时。
然后可以想到先 对\(a_1,a_2\) 做完,然后对于一块答案相同的部分存在数组里面,然后就可以用双指针来做,少了一个 log。复杂度 \(O(n \sqrt n)\)。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
int rd()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
int T,n,a1,a2,b1,b2,c;
unordered_map<int,PII> mp;
int calc(int l,int r,int x,int y)
{
if (x > r ||y <l) return 0;
if (l <= x && y <= r) return y-x +1;
if (x <= l && r <= y) return r-l+1;
if (x <= r && l <= x) return r-x+1;
if (y >= l && y <= r) return y-l+1;
}
struct node{int x,l,r;};
int main()
{
cin >>T;
vector<node>p1,p2;
while (T--)
{
//mp.clear();
p1.clear(),p2.clear();
cin >> a1 >> b1 >> a2 >> b2 >>n;
if (a1 < a2) swap(a1,a2),swap(b1,b2);
c= b2-b1;
int ans = 0;
for (int l = 1,r;l <= n;l=r+1)
{
if (l > a2)
{
p2.push_back({0,l,n});
break;
}
r = (a2/(a2/l));
r = min(r,n);
p2.push_back({a2/l,l,r});
}
for (int l = 1,r;l <= n;l=r+1)
{
if (l > a1)
{
p1.push_back({0,l,n});
break;
}
r = (a1/(a1/l));
r = min(r,n);
p1.push_back({a1/l,l,r});
}
reverse(p1.begin(),p1.end());
reverse(p2.begin(),p2.end());
int cnt1 = p1.size(),cnt2 = p2.size();
int j = 0;
rep(i,0,cnt1-1)
{
while (j < cnt2-1 && p2[j].x < p1[i].x-c) j++;
if (p2[j].x == p1[i].x-c)
{
ans += calc(p2[j].l,p2[j].r,p1[i].l,p1[i].r);
}
}
cout << ans << endl;
}
return 0;
}
Artoj P3184. 难题
模拟赛的好题,因没有特判挂了60 pts。
解法
可以发现构成 \(X\) 的方式都是 \(f_{2k-1}x + f_{2k}y = X\),其中 \(f_i\) 为斐波那契的第 i 项。然后发现这个式子形如 \(ax + by = c\) , 可以用 exgcd 来解。
做到这里以为没了,但实际上有个地方没有考虑到:算法在枚举斐波那契数列时,对于不同的 a,b,算出来的 x,y 是否可能重复。
我们假设存在:
我们发现解出来 \(y\) 为负数,不符合条件,因此 \(x,y\) 并不会重复。
但是比较特别的是当 \(c=0\) 时,\(x=0,y=0\),会被重复计算,因此需要特判 c = 0。
本题就做完了。
三元组
模拟赛遇到的好题,考验对 \(trie\) 树的理解。
一句话题意:给定一个序列 \(a\),\(n<=5 \times 10^5,a_i <= 2^{30}\),问 \((i,j,k)\) 满足 \((a_i\ xor\ a_j) < (a_j \ xor \ a_k)\) 的三元组数量。
std 是枚举 \(i\),但好像枚举 \(j\) 也可以做。
做法
枚举 \(i\),按位考虑发现可以从 \(i,k\) 从第几位开始不同考虑。
因为前几位相同时结果一定是相同的。
于是我们可以在trie树上枚举 \(a_k\) 从第 \(t\) 位开始与 \(a_i\) 不同。
而 j 可选的条件是 \(a_j\) 的第 \(t\) 位等于 \(a_i\) 的第 \(t\) 位。
因此对于每个 \(k\) 贡献就为 \(pre_{k,t,c} - pre_{i,t,c}\)。
可以将贡献拆开,每个节点维护 \(pre_{k,t,c}\) 的和还有 \(k\) 的数量。
\(c\) 为 \(a_i\) 的第 \(t\) 位。
此时对于每个 k 都要预处理出每一位,复杂度 \(n \log^2 V\),过不去。
考虑优化,我们发现对于每个 \(k\) ,需要查询的每次都是其所在的那一位,因此只需要保存所在那一位即可。
具体实现为对于 \(trie\) 树上每个节点保存一个 \(sum\),表示所有 k 的 \(pre_{k,c}\) 的和。
还有保存一个 \(cnt\),表示 k 的数量。
然后枚举 i,贡献为 \(sum - cnt * pre[i][c]\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i,a,b) for (int i = a;i <= b;i++)
#define ll long long
const int N = 5e5+5;
int n,a[N];
int son[N*32][2];
int cnt[N*32][2];
ll sum[N*32][2];
int tot;
ll pre[N][35][2];
void insert(int x,int id)
{
//cout << "insert" << x << endl;
int p = 0;
for (int i = 30;i >= 0;i--)
{
int ch = x >> i & 1;
if (!son[p][ch]) son[p][ch] = ++tot;
p = son[p][ch];
for (int c = 0;c < 2;c++)
cnt[p][c]++,sum[p][c] += pre[id][i][c];
// cout << p << ' ';
//cout << id << ' ' << i << ' ' << ch << endl;
}
//puts("");
}
int query(int id)
{
int p = 0;
ll res = 0;
for (int i = 30;i >= 0;i--)
{
int ch = a[id]>>i&1;
//cout << i << endl;
if (son[p][!ch])
{
// cout << i << endl;
int t2 = son[p][!ch],c = ch;
// cout <<t2 << ' ' << ch << endl;
res += 1ll*sum[t2][ch] - 1ll*cnt[t2][ch] * pre[id][i][c];
}
if (son[p][ch])
p = son[p][ch];
else break;
}
return res;
}
signed main()
{
cin >> n;
for (int i = 1;i <= n;i++) scanf("%lld",&a[i]);
for (int i = 1;i <= n;i++)
for (int j = 30;j >= 0;j--)
pre[i][j][(a[i]>>j&1)] = 1;
for (int j = 30;j >= 0;j--)
for (int k = 0;k < 2;k++)
for (int i = 1;i <= n;i++)
pre[i][j][k] += pre[i-1][j][k];
// for (int i = 1;i <= n;i++)
// {
// rep(j,0,3)
// rep(k,0,1)
// {
// cout << i << ' ' << j << ' ' << k << ' ' << pre[i][j][k] << endl;
// }
// }
ll ans = 0;
for (int i = n;i >= 1;i--)
{
// cout << query(i)<<endl;
ans += query(i);
insert(a[i],i);
}
cout << ans << endl;
return 0;
}
P4284 [SHOI2014] 概率充电器
感觉是一道非常好的题啊。
树形 DP + 概率期望。
题意:给定一棵树,每个点有概率自己带电,每条边也有概率可以传导电,问期望带电的点的个数。
做法
首先是一个非常简单经典的 \(trick\),看到期望但是值只有 \(1\),于是可以将期望转化成求每个点被点亮的概率之和。
于是考虑在树上做树形 DP。
首先我们可以设出 \(g_x\) 表示考虑 \(x\) 及其子树,使得 \(x\) 带电的概率。
这个转移非常好想:\(\large {g_x = 1 - \sum (1-g_{son} \times p_{x,son})}\)。
从反面考虑一下就做完了。
然后需要考虑父亲对当前 \(x\) 的影响,设 \(f[x]\) 表示 \(x\) 带电的概率。
我们发现父亲能对 \(x\) 产生影响当且仅当父亲不是由 \(x\) 点亮的。
因此设 \(p_1\) 表示父亲不是由 \(x\) 点亮的概率,则有:
\(\huge p_1 = \frac {(1-f_{fa})}{1-p_{fa,x}*g_x}\)
那么转移就为:
code
// Problem: P4284 [SHOI2014] 概率充电器
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4284
// Memory Limit: 256 MB
// Time Limit: 2000 ms
// Author: Eason
// Date:2024-09-26 16:32:38
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define adde(a,b) v[a].push_back(b)
#define rd read
#define PID pair<int,double>
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 5e5+5;
const double eps = 1e-8;
int n;
vector<PID> v[N];
double p[N];
double f[N];
double g[N];
double val[N];
int fa[N];
void dfs(int x,int fa)
{
if (v[x].size() == 1 && x != 1)
{
g[x] = p[x];
return;
}
double tp = 1;
for (auto [y,pc]:v[x])
{
if (y == fa) continue;
dfs(y,x);
val[y] = pc;
tp *= (1-(pc*g[y]));
}
tp *= (1-p[x]);
g[x] = 1-tp;
}
void dfs2(int x,int fa,double pc)
{
//cout << x << endl;
if (x != 1)
{
double p1 = f[fa];//p1表示父亲的子树除x以外的概率。
p1 = 1-p1;
if (1-g[x]*pc < eps)
{
f[x] = 1;
}
else
{
p1 /= (1-g[x]*pc);
p1 = 1-p1;
f[x] = g[x] + (p1 * pc) - g[x] * p1* pc;
}
}
for (auto [y,pc]:v[x])
{
if (y == fa) continue;
dfs2(y,x,pc);
}
}
int main()
{
cin >> n;
rep(i,1,n-1)
{
int x= rd(),y = rd(),p = rd();
double ds = p/100.0;
v[x].push_back({y,ds});
v[y].push_back({x,ds});
}
rep(i,1,n)
{
int x = rd();
p[i] = x/100.0;
}
if (n == 1)
{
cout << p[1] << endl;
return 0;
}
dfs(1,0);
f[1] = g[1];
dfs2(1,0,0);
double ans = 0;
rep(i,1,n)
{
//cout << i << ' ' << f[i]<< ' '<<g[i] << endl;
ans += f[i];
}
printf("%.6f",ans);
return 0;
}
C. Lazy Narek
VP打的,结果连 C 都做不出来 /qd /qd
题意:
给定 \(n\) 个字符串,任选几个按顺序拼在一起,设为字符串 \(S\),令 \(cnt\) 为 \(S\) 中 "narek" 的数量,问 \(\max \{S.size - cnt*5 \}\)。
一开始想的 \(DP\) 为 \(dp_{i}\) 表示考虑前 \(i\) 个的答案。然后转移考虑每个串末尾的可能。但是发现一个问题就是只能考虑到当前这个和前一个的贡献。换句话说 \(narek\) 跨过几个字符串的情况无法考虑,遂破防看题解。
正解
非常巧妙的 \(DP\) 啊。设 \(dp[i]\) 表示当前已经匹配了 \(i\) 个字符,正在匹配 "narek" 中的第 \(i-1\) 个的答案。
为什么能这样设置呢?因为我们可以发现答案与当前是第几个字符串没有什么关系,而且从一个一个字符匹配 "narek" 的角度来思考可以解决跨字符串的问题,并且每个字符串选或不选只关系到当前匹配到第几个字符。
其实也可以看成是节省了一维的空间。
考虑转移。
枚举第 \(i\) 个字符串,枚举当前匹配到了第 \(k\) 位,那么只需要扫一遍字符串,从 \(k\) 位往后循环匹配,记录贡献。
设扫完后匹配到了第 \(now\) 位,则转移就为:\(dp[now] = max(dp[now],dp[k]+res)\)
code
// Problem: C. Lazy Narek
// Contest: Codeforces - Codeforces Round 972 (Div. 2)
// URL: https://codeforces.com/contest/2005/problem/C
// Memory Limit: 256 MB
// Time Limit: 2000 ms
// Author: Eason
// Date:2024-09-27 10:51:26
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define adde(a,b) v[a].push_back(b)
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 2e3+5;
int t;
int n,m;
int dp[10];
int tmp[10];
string str[N];
string narek = "narek";
int main()
{
cin >> t;
while (t--)
{
cin >> n >> m;
rep(i,1,n) cin >> str[i];
memset(dp,-INF,sizeof dp);
dp[0] = 0;
rep(i,1,n)
{
memcpy(tmp,dp,sizeof tmp);
rep(k,0,4)
{
int now = k,res = 0;
rep(j,0,m-1)
{
int idx = narek.find(str[i][j]);
if (idx == -1) continue;
if (idx != now) res--;
else res++,now = (now+1)%5;
}
tmp[now] = max(tmp[now],dp[k] + res);
}
memcpy(dp,tmp,sizeof dp);
}
int ans = 0;
for (int i = 0;i < 5;i++) ans = max(ans,dp[i]- 2 * i);
cout << ans << endl;
}
return 0;
}
关于最后为什么要写
for (int i = 0;i < 5;i++) ans = max(ans,dp[i]- 2 * i);
而不是输出 \(dp[0]\) ,这是因为到最后不一定是刚好匹配完,可能多匹配了几位。
HACK:
1
2 5
ppppn
arekn
如这个数据,最后会匹配成 "narekn",因此 \(dp[1] - 1\times 2\) 才是答案。
CSP-S 模拟赛 A. 序列
给点序列 \(a_1,a_2,\cdots ,a_n\),问有多少非负整数序列 \(x_1,x_2,\cdots,x_n\) 满足:
- \(\forall i,0 \le x_i \le a_i\)。
- 满足 \(x_1 | x_2 | \cdots | x_n = x_1 + x_2 + \cdots + x_n\)。
\(n \le 16,a_i < 2^{60}\)
状压+数位 DP 好题。
条件二可以转化为 \(x\) 两两并为 \(0\)。按位考虑,即每一位至多一个 \(1\)。
考虑从高位开始给每一位填数,那么需要判断是否超过 \(a_i\) ,可以想到用数位 \(DP\)。
对于每一个数字记录当前是否达到最大限制,即数位 DP 中的 \(limit\)。
用一个 \(16\) 位二进制记录即可。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define adde(a,b) v[a].push_back(b)
#define addev(a,b,c) v[a].push_back({b,c});
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 25,M = 66,mod = 998244353;
int n,a[N];
int num[N][M];
int f[M][(1<<16)+10];
void solve()
{
cin >> n;
rep(i,1,n) a[i] = rd();
for (int i = 1;i <= n;i++)
{
for (int j = 1;j <= 60;j++)
{
num[i][j] = (a[i]>>(j-1))&1;
}
}
for (int j = 0;j < (1<<n);j++) f[0][j] = 1;
for (int i = 1;i <= 60;i++)
{
for (int j = 0;j < (1<<n);j++)
{
int cur = 0;
for (int k = 1;k <= n;k++)
{
if ((j>>(k-1)&1) && num[k][i]==0) cur += (1<<(k-1));
}
(f[i][j] += f[i-1][cur])%=mod;
for (int k = 1;k <= n;k++)
{
int sj = (j>>(k-1))&1;
int limit = 1;
if (sj == 1) limit = num[k][i];
if (limit == 0) continue;
int ncur = cur;
if (sj) ncur += (1<<k-1);
(f[i][j] += f[i-1][ncur])%=mod;
}
}
}
cout << f[60][(1<<n)-1] << endl;
}
signed main()
{
int t;t = 1;
while(t--)
{
solve();
}
return 0;
}
A. 卡牌游戏
来源:CSP-S Day 8 模拟赛
你有两个属性攻击力 \(A\) 和增量 \(D\),初始为 \(0\)。
还有一个 \(S\) 表示总伤害,初始为 \(0\)。
你将进行 \(n\) 轮游戏,每轮有 \(a_i,b_i,c_i\),每轮开始 A += D。
接下来选择一项:
- \(S \ += A+a_i\)
- \(D \ += b_i\)
- \(A\ += c_i\)
问 \(S\) 的最终最大值。
考虑计算每一项操作对于 \(S\) 的贡献,考虑需要计算记录哪些东西。
操作一可以直接加。
对于操作三,若我们知道后面攻击了 \(cnt\) 次,则我们可以知道该操作对于 \(S\) 的贡献为 \(cnt \times c_i\)。
但是操作二似乎没有那么好记录,发现增量对于 \(S\) 的贡献大概是 \(\sum_j (j-i)*b_i\),其中 \(j\) 是 \(i\) 后面选择操作 \(1\) 的游戏轮。
那么我们发现只需要知道 \(i\) 后面所有的 \(j\) 的和。
则状态设计为:\(dp_{i,j,k}\) 表示考虑 \([i,n]\) 轮游戏,选择 \(j\) 轮游戏进行进攻,这 \(j\) 轮游戏的轮数和为 \(k\)。
则转移就非常简单了。
感觉是一道 DP 好题,需要仔细研究状态的设计。
const int N = 105;
int n;
int a[N],b[N],c[N];
ll dp[105][105][5005];
void solve()
{
cin >> n;
for (int i = 1;i <= n;i++) a[i] = rd(),b[i] = rd(),c[i] = rd();
memset(dp,-INF,sizeof dp);
dp[n+1][0][0] = 0;
for (int i = n;i >= 1;i--)
{
for (int j = 1;j <= n-i+1;j++)
{
for (int k = i*j;k <= n*(n+1)/2;k++)
{
ll gx1 = -INF,gx2 = -INF,gx3 = -INF;
if (j >= 1 && k >= i)
gx1 = dp[i+1][j-1][k-i] + a[i];
gx2 = dp[i+1][j][k] + 1ll*j * c[i];
gx3 = dp[i+1][j][k] + 1ll*(k-j*i)*b[i];
dp[i][j][k] = max({gx1,gx2,gx3});
}
}
}
ll ans = 0;
for (int j = 1;j <= n;j++)
for (int k = 0;k <= n*(n+1)/2;k++)
ans = max(ans,dp[1][j][k]);
cout << ans << endl;
}
时间复杂度 \(O(n^4)\),有点小卡。
A. 中位数
来源:2024 CSP-S 模拟赛 Day 15
你有一个序列 \(a_1,a_2,…,a_n\)。
定义 \(b_{l,r}\) 表示序列 \(\{a_i\}_{l≤i≤r}\) 的中位数。定义 \(c_{l,r}\) 为序列 \(\{b_{i,j}\}_{l≤i≤j≤r}\) 的中位数。
求 \(\{c_{i,j}\}_{l≤i≤j≤r}\) 的中位数是多少。
对于一个大小为 \(m\) 的序列,我们定义它的中位数为第 \(\left \lceil \frac{m}{2}\right \rceil\) 小的数字。
一个经典的trick:\(a_i \ge x \rightarrow 1,a_i < x \rightarrow 0\)
看到中位数,可以想到先二分中位数为 \(x\)。
然后我们将大于等于 \(x\) 的数看成 \(1\),小于 \(x\) 的数看成 \(0\),得到一个新序列。
则通过我们在新序列得到的答案就可以知道答案与 \(x\) 的大小关系。
在新序列上考虑原问题。
首先 \(b_{l,r}\) 可以通过一维前缀和直接判断,当 \(sum_r-sum_{l-1} > \frac{(r-l+1)}{2}\),\(b_{l,r}\) 为 \(1\)。
同理,\(c_{l,r}\) 也可以用二维前缀和算出来。
然后这题就做完了。
和这题同个 trick 的经典题目:P2824 [HEOI2016/TJOI2016] 排序
题意是区间排序操作,在操作最后单点查询。
做法是二分答案,然后 \(a_i \ge x \rightarrow 1,a_i < x \rightarrow 0\)。
那么区间排序就可以用区间赋值 \(0/1\) 来完成。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define adde(a,b) v[a].push_back(b)
#define addev(a,b,c) v[a].push_back({b,c});
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 1e5+5;
int n,m,p[N];
struct node
{
int op,l,r;
}Q[N];
int idx;
struct node2
{
int sum,tag=-1;
}tr[N<<2];
void stf(int k,int l,int r,int c)
{
tr[k].sum = (r-l+1) * c;
tr[k].tag = c;
}
void psd(int k,int l,int r)
{
if (tr[k].tag != -1)
{
int mid = l + r >> 1;
stf(k<<1,l,mid,tr[k].tag);
stf(k<<1|1,mid+1,r,tr[k].tag);
tr[k].tag = -1;
}
}
void modify(int k,int l,int r,int x,int y,int c)
{
if (x > r || y < l) return;
if (x <= l && r <= y)
{
stf(k,l,r,c);
return;
}
int mid = l + r >> 1;
psd(k,l,r);
modify(k<<1,l,mid,x,y,c);
modify(k<<1|1,mid+1,r,x,y,c);
tr[k].sum = tr[k<<1].sum + tr[k<<1|1].sum;
}
int query(int k,int l,int r,int x,int y)
{
if (x > r || y < l) return 0;
if (x <= l && r <= y) return tr[k].sum;
int mid = l+ r >> 1;
psd(k,l,r);
return query(k<<1,l,mid,x,y) + query(k<<1|1,mid + 1,r,x ,y);
}
void build(int k,int l,int r,int x)
{
if (l == r)
{
tr[k] = {(p[l] >= x),0};
return;
}
int mid =l + r >> 1;
build(k<<1,l,mid,x);build(k<<1|1,mid+1,r,x);
tr[k].sum = tr[k<<1].sum + tr[k<<1|1].sum;
tr[k].tag = -1;
}
int check(int x)
{
build(1,1,n,x);
for (int i = 1;i <= m;i++)
{
auto[op,l,r] = Q[i];
int cnt = query(1,1,n,l,r);
// cout << cnt << endl;
if (cnt == 0) continue;
modify(1,1,n,l,r,0);
if (op == 1) modify(1,1,n,l,l+cnt-1,1);
else modify(1,1,n,r-cnt+1,r,1);
}
return query(1,1,n,idx,idx);
}
void solve()
{
cin >> n >> m;
for (int i = 1;i <= n;i++) p[i] = rd();
rep(i,1,m)
{
Q[i] = {rd(),rd(),rd()};
}
idx = rd();
int l = 1,r = 1e5;
while (l < r)
{
int mid = l + r+1 >> 1;
if (check(mid)) l = mid;
else r = mid -1;
}
//cout << check(6) << endl;
cout << l << endl;
}
int main()
{
int t;t = 1;
while(t--)
{
solve();
}
return 0;
}
[ABC282Ex] Min + Sum
一道最小值分治的 trick。
又称笛卡尔树分治。
题意:求 (l,r) 对数满足 \(1\le l \le r \le n\) 且 \(\sum_{i=l}^rB_i+\min_{i=l}^rA_i\le S\)
做法考虑分治,将 \(mid\) 设为区间最小值的位置,这一步可以用 \(st\) 表做。
然后将将原区间 \([l,r]\) 划分成 \([l,mid-1],[mid+1,r]\) ,因此我们只需要考虑 \((l,r)\) 跨过 \(mid\) 的贡献。
我们发现由于区间和的单调性,在确定 \(l,r\) 其中一个时,另一个可以二分求得。
同时为了保证复杂度,我们枚举左右中长度较小的区间,于是本题就做完了。
复杂度分析
题解区还看到一种更nb的做法,直接用单调栈算出该值作为最小值的最左和最右,然后直接枚举左右区间中更小的区间,二分计算。
这两种方法的复杂度都可以用从笛卡尔树的角度证明。
考虑每个位置 \(i\) 在暴力枚举中产生了多少贡献。
设当前区间节点为 \(x\),而位置 \(i\) 在 \(x\) 的子树 \(y\) 中,且 \(y\) 是 \(x\) 较小的子树。
那么在枚举完子树 \(y\) 后,下一次再枚举到 \(i\) 时,就应是 \(x\) 作为子树进行枚举。
而又有 \(siz_x \ge 2 \times siz_y\) ,于是我们发现每一次枚举 \(i\) 的时候,其所在区间就翻倍。
也就是说每个位置最多被枚举 \(O(n \log n)\) 次。
类似启发式合并。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define mem memset
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 2e5+5;
int n,a[N],b[N];
int sum[N];
int S;
int dp[N][25];
void prework()
{
for (int i = 1;i <= n;i++) dp[i][0] = i;
for (int j = 1;j <= 20;j++)
for (int i = 1;(i+(1<<j)-1) <= n;i++)
{
int t1 = dp[i][j-1],t2 = dp[i+(1<<(j-1))][j-1];
if (a[t1] < a[t2]) dp[i][j] = t1;
else dp[i][j] = t2;
}
}
int query(int l,int r)
{
int k = log2(r-l+1);
int t1 = dp[l][k],t2 = dp[r-(1<<k)+1][k];
return ((a[t1] < a[t2])?t1:t2);
}
int ans;
int calcl(int id,int l,int r,int num)
{
l--;
while (l < r)
{
int mid = l + r + 1>> 1;
if (sum[mid] - sum[id-1] <= num) l = mid;
else r = mid - 1;
}
return l;
}
int calcr(int id,int l,int r,int num)
{
r++;
while (l < r)
{
int mid =l + r>>1;
if (sum[id]-sum[mid-1] <= num) r = mid;
else l = mid + 1;
}
return l;
}
void solve(int l,int r)
{
if (l > r) return;
int mid = query(l,r);
if (mid-l < r-mid)
{
for (int i = l;i <= mid;i++) ans += calcl(i,mid,r,S-a[mid]) - mid+1;
}
else{
for (int i = mid;i <= r;i++) ans += mid - calcr(i,l,mid,S-a[mid]) + 1;
}solve(l,mid-1);solve(mid+1,r);
}
signed main()
{
cin >> n >> S;
rep(i,1,n) a[i] =rd();
rep(i,1,n) b[i] =rd(),sum[i] = sum[i-1] + b[i];
prework();
solve(1,n);
cout << ans << endl;
return 0;
}
CF1956D Nene and the Mex Operator
*2000
贼有趣的 \(DP\) 加构造。
题意
给定长度为 \(n\) 的序列 \(a_i\),满足 \(1\leq n\leq 18,0\leq a_i\leq 10^7\)。定义一次操作为将 \([l,r]\) 区间赋值为 \(a_l,\ldots,a_r\) 的 \(\mathrm{mex}\) 值,求在 \(5\times 10^5\) 次操作之内序列和的最大值,并给出操作序列。
分析
题目要求求出最大值并给出构造方案。
我们先思考题目给出的操作有什么性质。
由于题目给出的限制 \(cnt<=5 \times 10^5\) 非常大,所以我们大胆尝试。
通过手玩样例,我们猜测对于任意区间 \([l,r]\) 我们都可以将其中每个数变成 \(r-l+1\) ,即区间长度。
等下再证明这玩意,先看看知道了这条性质怎么求出最大值。
考虑 \(DP\),设 \(f_{i}\) 表示前 \(i\) 个能得到的最大值,直接枚举 \(j\) 转移:
过程中记录一下上次的转移点,就可以知道操作了哪些区间。
再考虑刚才的操作,我们假设区间长度 \(len = 5\)
而最后的状态是
\(5 \ 5 \ 5 \ 5 \ 5\)
想要达成这个状态,必不可少的是
\(4 \ 3 \ 2 \ 1 \ 0\)
设 \(func(l,r)\) 表示对 \([l,r]\) 进行一次操作。
设 \(g_i\) 表示在原区间形成 \(i,i-1,i-2,\dots 0\) 的所需操作集。
我们发现 \(g_4 = g_3 + func(1,5) + g_3\)
于是我们可以得出 \(g_i = g_{i-1}+func(l,r)+g_{i-1}\)
而操作数刚好为 \(f_i = 2^{i}-1\)
\(2^{18} + 18 \le 5 \times 10 ^ 5\) 稳稳通过!
2028E - Alice's Adventures in the Rabbit Hole
题意
给定一棵树,皇后想处死爱丽丝,而爱丽丝想逃出去。爱丽丝若在叶子节点则被处死,在根节点则逃出。
每次操作都有 \(\frac{1}{2}\) 的概率由皇后或爱丽丝来操作一次操作可以将爱丽丝移动到相邻的节点。
问爱丽丝起点在 \(1,2,\dots , n\) 时,爱丽丝逃出的概率。
做法
先考虑一条链的情况:
\(1,2,3,\dots,d,\dots,n\)
假设起点在 \(i\) 的答案为 \(f_i\)。
则有
即 \(f\) 是一个等差数列,所以:
通项为:
我们在树上做一个短链剖分。
解释一下,就是把树链剖分的重儿子变成到最近叶子的距离最小的儿子。
对于每条短链,我们需要额外算上链顶的父亲。
然后就可以将其当做链上的情况了。
假设当前在 \(i\),\(p\) 是 \(i\) 跑到链的父亲的概率,则 \(1-p\) 是 \(i\) 跑到叶子的概率。
因为当爱丽丝跑到当前链的父亲上时,此时已经换了一条链了,所以需要分开考虑。
则有:
而 \(p\) 可以用上面的公式算。
于是做完了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define INF 0x3f3f3f3f
#define re register
#define int ll
#define PII pair<int,int>
#define rep(k,a,b) for (int k = a;k <= b;k++)
#define adde(a,b) v[a].push_back(b)
#define addev(a,b,c) v[a].push_back({b,c});
#define rd read
int read()
{
int f=1,k=0;char c = getchar();
while(c <'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9')k=(k<<1)+(k<<3)+(c^48),c=getchar();
return k*f;
}
const int N = 2e5+5,mod = 998244353;
int n;
int siz[N],top[N],son[N];
int dep[N];
int dis[N];
int fat[N];
int inv[N];
vector<int> v[N];
int ksm(int a,int b)
{
int res= 1;
while (b)
{
if (b & 1) res = 1ll*res * a%mod;
a = 1ll * a * a %mod;
b >>= 1;
}
return res;
}
void dfs1(int x,int fa)
{
fat[x] = fa;
dep[x] = dep[fa] + 1;
for (auto y : v[x])
{
if (y == fa) continue;
dfs1(y,x);
if (!son[x] || dis[son[x]] > dis[y]) son[x] = y;
}
if (son[x])
dis[x] = dis[son[x]] + 1;
}
int f[N];
void dfs2(int x,int tp)
{
top[x] = tp;
int len = dis[tp]+1 + (tp!=1);
int d = dep[x] - dep[tp]+2-(tp==1);d = len-d+1;
f[x] = (d-1)*(ksm(len-1,mod-2))%mod*f[fat[tp]]%mod;
if (x==1)f[x]=1;
if (son[x]) dfs2(son[x],tp);
for (auto y : v[x])
{
if (y == fat[x] || y == son[x]) continue;
dfs2(y,y);
}
}
void solve()
{
cin >> n;
rep(i,1,n-1)
{
int x= rd(),y = rd();
v[x].push_back(y);
v[y].push_back(x);
}
dfs1(1,1);
dfs2(1,1);
rep(i,1,n) cout << f[i] << ' ';
puts("");
rep(i,0,n) v[i].clear(),f[i] = top[i] = siz[i] = dep[i] = dis[i] = fat[i] = son[i] = 0;
}
signed main()
{
rep(i,1,2e5) inv[i] = ksm(i,mod-2);
int t;t = rd();
while(t--)
{
solve();
}
return 0;
}
A. ⚔️
来源:2024 NOIP 模拟赛 Day 12
抽象题目加抽象解法。
笛卡尔树 + 树形 dp
题意
有 \(n\) 个人站成一行,能力值分别为 \(a_1, a_2, \dots, a_n\)。
会进行 \(n - 1\) 轮比赛。每次比赛,裁判会选出两个序列中相邻的选手,其中能力值更高的选手将获得胜利,而另一位选手会被淘汰。当两个人能力相同的时候,你可以任意选择胜者。败者会离开队伍,而胜者的能力值会增加 \(1\),并且剩下的人合并成一行,继续进行比赛。
你可以任意选择比赛顺序和相同情况下的胜者,问最后哪些选手可能成为最后的赢家。从小到大输出这些选手的编号。
做法
我们对于原序列建笛卡尔树,首先最大值是一定可以赢的。
然后我们观察到每个节点可以将其子树所有点吃掉。
设选手 \(i\) 能否赢为 \(ans_i\)。
于是我们可以列出 dp:\(f_i\) 表示我们将子树 \(i\) 删掉,取代为一个权值为 \(f_i\) 的数且 \(ans_i=1\),满足 \(f_i\) 最小。
可以得到:
-
\(f_i \ge a_{fa}\)
-
\(f_i \ge f_{fa} - (siz_{fa} - siz_i)\)
于是 \(f_i\) 为两者取个 \(max\)。
从上往下做就做完了。
day4 C 互不相同
给定 \(a_i\),定义一次操作为选择一段子区间加上任意整数 \(x\),问至少几次操作使得全局互不相同。
假设做了 k 次区间加法 产生 \(2k\) 个端点
可以发现端点之间每个区间内部都是互不相同的。
那么原题可以等价为我们选择一个左端点每次贪心的选直到有相同的就新开一个区间。
此时需要操作的次数 = (区间数+1)/2
因为一次操作会产生两个端点。
此时还有一个问题是左右两端的贡献计算,一种方法是枚举左端点找出对应右端点,
另一种方法是强行将原序列拼成一个环,即将原数组复制一遍,往后倍增跳满足 r-l+1<=n
便能自然地算出左右端贡献了。
https://www.luogu.com.cn/problem/CF1237F
CF1237F Balanced Domino Placements
题目描述
给定一个 \(h\) 行 \(w\) 列的方格棋盘,上面已经放置了一些多米诺骨牌。每个多米诺骨牌恰好覆盖棋盘上相邻的两个格子,每个格子最多只能被一个多米诺骨牌覆盖。
我们称一个多米诺骨牌的放置方式为“完全平衡”,如果没有任何一行或一列中存在被两个不同多米诺骨牌覆盖的两个格子。换句话说,每一行和每一列要么没有被覆盖的格子,要么只有一个被覆盖的格子,要么有两个被覆盖的格子且这两个格子属于同一个多米诺骨牌。
现在给定一个已经完全平衡的多米诺骨牌放置方式,问有多少种方法可以在此基础上再放置零个或多个多米诺骨牌,且仍然保持完全平衡。请输出方案数对 \(998\,244\,353\) 取模的结果。
当前的网格中有部分行部分列被叉掉,问还能放骨牌的方案数。
直接思考,先放竖着的再放横着的,发现竖着的骨牌会叉掉两行一列
对于横着的产生影响,互不独立,不方便计算。
简化问题,考虑只有竖着的。
变成了有若干固定行不可选,一次得选两行的方案数,dp。\(f_{i,j}\) 表示前 \(i\) 行选 \(j\) 次的方案数。
当然此时这些选出来的骨牌的列是错开的而且不固定。
算方案数我们只需要在剩下没有被叉的列中选出 \(j\) 列即可
因此最终为 \(f_{n,j} \times A_{cnth}^{j}\)
同理我们也可以计算出 \(g_{m,i}\) 表示只考虑横着的。
此时似乎横竖会互相影响,我们这样想:
不具体考虑每一个骨牌的具体位置,而是考虑使他们错开,互不影响。
比如说假设有 \(i\) 个竖着的,\(j\) 个横着的。\(cnt_x \ cnt_y\) 分别是没有被叉的行和列的数量
\(f_{n,i}\) 算出了 \(n\) 行选出 \(i\) 张骨牌的方案数,
考虑这 \(i\) 张骨牌能放在哪 \(i\) 列里?
可以在 \(cnt_x - 2\times j\) 个列中选择,因为一张横着的会叉掉两列。
乘法原理乘上 \(A_{cntx-2\times j}^{i}\)
此时还有一点问题。
横着的同时会叉掉一行,因此横着的也只能在 \(cnt_y - 2\times i\) 行中选择
再乘上 \(g_{m,j} \times A_{cnt_y-2\times i}^{j}\)
得到最终答案。
C. 异或可达
模拟赛 day5 C
题意
给定 \(m\) 条边和常量 \(k\),\(q\) 个询问,给定\(d\)。
问 \((x,y)\) 数量
满足 \(x < y\) 且 \(x,y\) 联通且每条边权 \(c_j \ xor \ d < k\)。
做法
要维护无向图连通性。
想到并查集。
查询并不简单,考虑将查询离线并放在一起。
\(c_j \ xor \ d < k\) 限制条件提醒我们将问题放在 trie 树上思考。
我们考虑在trie树上分治。
假设当前在 \(x\) ,只考虑当前位。
当 k = 1,d = 1
那么 $son_{x,0} $ 整棵子树下的边都应被选入。
于是我们暴力将 \(son_{x,0}\) 整棵子树加入,并继续在 \(son_{x,1}\) 分治。
加入后我们用可撤销并查集将操作撤销(类似于线段树分治
其他情况同理。
于是,每条边都只会被添加 \(\log V\) 次,问题在 \(n \log V \log n\) 的时间复杂度被解决。
模拟赛 day6 D. 阻碍
题意
带权无向图,问那些边删除后使得 \(1\) 到 \(n\) 的最短路 \(dist_{1,n}\) 加 \(1\)。
这是一道最短路问题,考虑先把最短路径图建出来。
此时得到一个 DAG。
DAG 上思考问题不太简单。
不妨我们考虑该 DAG 上的以 \(1\) 为根搜索树。
此时会发现一些性质:
- 满足条件的边一定不会在搜索树之外
因为删除后,搜索树的最短路依旧是 \(dist_{1,n}\),因此满足条件的边在 \(1-n\) 的路径上
-
考虑维护每条边删除后的最短路,搜索树外的一条边 \((u,v)\) 可以更新他们路径上的所有边的答案
贡献为:\(dist_{1,u} +dist_{v,n} + w(u,v)\)
于是得到做法,我们将树外的边按照上面的贡献从小到大排序,每次覆盖一段路径。
可以使用并查集维护,\(f_i\) 表示 \(i\) 上最近的没有被覆盖的点。初始 \(f_i = i\)
最后查一遍在 \(1-n\) 路径上所有的边就做完了。
kruskal 重构树
P11907 [NHSPC 2023] F. 恐怖的黑色魔物
在一个三维的网格上,可以移动到相邻的点。
点权值 \(d\) 为最近关键点的曼距。
\(q\) 次查询问 \(s \rightarrow t\) 路径上最小权值最大。
做法
首先 \(d\) 可以用多源bfs 解决。
最小权值最大容易想到最大生成树,点权可以通过 \(w(u,v) = \min (d_u,d_v)\) 解决。
或者直接每次选最大权值的点找已经被加入的邻居去更新生成树。
可以建 kruskal 重构树解决。
值得注意的一个 trick 是在建kruskal重构树时不需要跑 lca 来最小权值。
我们的做法是发现合并两个联通块直接只需要任意两个点连上边即可,
因此我们不需要新建点,启发式合并两棵树的根,边权为原点权。
与原重构树性质一致。
树高便变为了 \(O(\log n)\),只需要两个点往上暴力跳即可。
代码大概是这样:
find(x);find(y);
if (dep[x] < dep[y])swap(x,y);
while (dep[x] > dep[y]) mx = min(maxn,fval[x]),x = fa[x];
while (x != y)
{
mx = min({mx,fval[x],fval[y]});
x = fa[x],y = fa[y];
}
int find(int x)
{
// cout<< x <<endl;
if (fa[x] != x)
{
int t = find(fa[x]);
dep[x] = dep[fa[x]] + 1; // 维护深度
return t ;
}
dep[x] = 1;
return fa[x];
}

浙公网安备 33010602011771号