目录
JMU
第一届程序设计金秋赛题解
出题表:
签到题
5+5
出题人:贝
题意
主人公 5 5 5年前 5 5 5岁, 5 5 5年后几岁?
思路
这是一道小学数学题,要求选手画线段图(bushi)。
因为主人公 5 5 5年前 5 5 5岁,说明现在是 10 10 10岁,也就是说 5 5 5年后 15 15 15岁。
代码实现
#include<stdio.h>
int main()
{
printf("15");
}
4514
出题人:贝
题意
45 45 45枚硬币在桌上, 14 14 14枚正面朝上,贝贝蒙住了你的眼睛,你无法通过触觉摸出硬币的正反面,现在贝贝要求你执行以下两种操作各一次(两种操作的顺序不限),使得最后左右两堆的硬币正面朝上的一样多:
- 将硬币分成左、右两堆,且左边的硬币数量小于右边的硬币数量(因为你的眼睛被蒙住了,所以每一堆硬币的正反面是随机的)。
- 将某一堆全部的硬币的正反面都翻转过来(若在未执行1操作时执行该操作,则将桌面所有硬币都翻转正反面)
假设执行1操作时,将硬币分成了数量分别为 a , b a,b a,b的两堆,其中 1 ≤ a ≤ 22 < b ≤ 44 1\le a\le 22< b \le 44 1≤a≤22<b≤44且 a + b = 45 a+b=45 a+b=45,贝贝希望你能告诉他 a a a的值是多少?
思路
首先执行1操作,将硬币分为两堆,假设左边有 x x x个,右边则有 45 − x 45-x 45−x个
设左边的正面朝上的数量为 y y y,根据题意可以得到右边有 14 − y 14-y 14−y个正面朝上
然后我们将左边的所有硬币翻面,也就是正面数量等价之前的反面数量 x − y x-y x−y
最后需要让二者相等,即 x − y = 14 − y x-y=14-y x−y=14−y,易得 x = 14 x=14 x=14
故最后输出14
即可
当假设题意中的正面朝上数量是给定的一个数
a
a
a也行,将上述推导中的
14
14
14换成
a
a
a即可,最后的输出为
a
a
a。为了降低难度,怕选手雀氏不会做,所以给定了指定的数字14
,让选手可以提交若干发一定正确。但是我又不能将提交的次数设置的过小,否则就对于想正解的选手不公平。
代码实现
#include<stdio.h>
int main()
{
printf("14");
}
小W的砖块
出题人:吴
题意
做两次区间染色,求颜色相交的区间长度。即求两个区间的公共长度。
思路1
因为区间范围给的比较小,可以直接模拟两次染色然后统计答案。多组数据记得清空数组。
时间复杂度 O ( n ) O(n) O(n)。
代码实现1
#include<bits/stdc++.h>
using namespace std;
int col[1005];
void solve(){
memset(col,0,sizeof col);
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
for(int i=l1;i<=r1;i++) col[i]=1; // 第一次染色
int ans=0;
for(int i=l2;i<=r2;i++) if(col[i]) ans++; // 第二次并不需要真的去染色, 统计一下答案即可
cout<<ans<<endl;
}
signed main(){
int _;cin>>_;
for(int cas=1;cas<=_;cas++){
solve();
}
return 0;
}
思路2
不妨设 l 1 ≤ l 2 l_1 \le l_2 l1≤l2,分三种情况讨论一下。
- 两个区间不相交,即 r 1 < l 2 r_1<l_2 r1<l2。答案应该为0。
- 一个区间完全包含于另一个区间,即 r 2 ≤ r 1 r_2 \le r_1 r2≤r1。答案应该为小区间长度,即 r 2 − l 2 + 1 r_2-l_2+1 r2−l2+1。
- 两个区间部分相交。答案应该为相交部分长度,即 r 1 − l 2 + 1 r_1-l_2+1 r1−l2+1。
时间复杂度 O ( 1 ) O(1) O(1)。
画图更清晰哦,注意设置前提条件 l 1 ≤ l 2 l_1 \le l_2 l1≤l2。
代码实现2
#include<bits/stdc++.h>
using namespace std;
void solve(){
int l1,r1,l2,r2;cin>>l1>>r1>>l2>>r2;
if(l1>l2){
swap(l1,l2);
swap(r1,r2);
}
if(r1<l2) cout<<0<<endl; // 区间不相交
else if(r2<=r1) cout<<r2-l2+1<<endl; // 区间完全包含
else cout<<r1-l2+1<<endl; // 部分相交
}
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int _;cin>>_;
while(_--) solve();
return 0;
}
正常作息的贝贝
出题人:周
签到题,按题意模拟即可。
#include <bits/stdc++.h>
using namespace std;
void solve()
{
int n;
cin >> n;
int h = n / 60;
int m = n % 60;
printf("%02d:%02d\n", (22 + h) % 24, m);
}
int main()
{
int T;
cin >> T;
while (T--)
solve();
return 0;
}
简单题
擅长算术的琪露诺
出题人:吴
感谢周大佬写的题面orz
题意
给定一个长度为
n
n
n 的数组
a
[
]
a[]
a[],求
∑
i
=
1
n
∑
j
=
1
n
a
i
∗
a
j
\sum_{i=1}^{n}\sum_{j=1}^{n}a_i*a_j
i=1∑nj=1∑nai∗aj
思路
∑ i = 1 n ∑ j = 1 n a i ∗ a j = ∑ i = 1 n a i ∑ j = 1 n a j = ( ∑ i = 1 n a i ) ∗ ( ∑ j = 1 n a j ) = ( ∑ i = 1 n a i ) 2 \begin{align} \sum_{i=1}^{n}\sum_{j=1}^{n}a_i*a_j &= \sum_{i=1}^{n}a_i\sum_{j=1}^{n}a_j \\ &= (\sum_{i=1}^{n}a_i)*(\sum_{j=1}^{n}a_j) \\ &= (\sum_{i=1}^{n}a_i)^2 \end{align} i=1∑nj=1∑nai∗aj=i=1∑naij=1∑naj=(i=1∑nai)∗(j=1∑naj)=(i=1∑nai)2
于是答案就是和的平方。时间复杂度 O ( n ) O(n) O(n)。
代码实现
#include<bits/stdc++.h>
using namespace std;
void solve(){
int n;cin>>n;
long long ans=0;
for(int i=1;i<=n;i++){
long long x;cin>>x;
ans+=x;
}
cout<<ans*ans<<endl;
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int _;cin>>_;
while(_--) solve();
return 0;
}
中档题
上帝造裸题的七分钟
出题人:周
- 值得注意的是,因为模数 p p p不一定是质数,所以很难求出逆元,故本题需避免求逆元,以及用除法
- 对于萌新选手,此处给予提醒,
double
会有精度误差,只能保留15-16
位的有效数字,所以也不能用double
设 p r e [ i ] pre[i] pre[i]表示第 1 1 1个数到第 i i i个数的积, s u f [ i ] suf[i] suf[i]表示第 i i i个数到第 n n n个数的积。同时令 p r e [ 0 ] = 1 , s u f [ n + 1 ] = 1 pre[0]=1,suf[n+1] = 1 pre[0]=1,suf[n+1]=1
那么 b [ i ] b[i] b[i]就可以表示成
b [ i ] = s u f [ i + 1 ] × p r e [ i − 1 ] b[i] = suf[i+1]\times pre[i-1] b[i]=suf[i+1]×pre[i−1]
直接计算即可(注意计算时需要取模)。时间复杂度为 O ( n ) O(n) O(n)
#include <bits/stdc++.h>
using namespace std;
#define LL long long
#define endl '\n'
void solve()
{
int n, p;
cin >> n >> p;
vector<LL> a(n + 2), pre(n + 2), suf(n + 2);
for (int i = 1; i <= n; ++i)
cin >> a[i];
pre[0] = 1;
suf[n + 1] = 1;
for (int i = 1; i <= n; ++i)
pre[i] = pre[i - 1] * a[i] % p;
for (int i = n; i >= 1; --i)
suf[i] = suf[i + 1] * a[i] % p;
for (int i = 1; i <= n; ++i)
cout << pre[i - 1] % p * suf[i + 1] % p << " ";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
solve();
return 0;
}
静神的外卖红包
出题人:吴
题意
给一个不包含0的数字串,删去 m m m 个数,求能得到的字典序最小串。
思路
不难看出,如果第 i i i 位数字大于第 i + 1 i+1 i+1 位数字,删掉第 i i i 位数字能让数字串变小且最优。
证明:
设数字串为 S S S。若 S 1 > S 2 S_1>S_2 S1>S2,删掉 S 1 S_1 S1 得到的数字串字典序一定比删除 S 2 S_2 S2 得到的数字串字典序小。(删掉 S 1 S_1 S1 的串为 T 1 = S 2 S 3 S 4 . . . S n T_1=S_2S_3S_4...S_n T1=S2S3S4...Sn,删掉 S 2 S_2 S2 的串变为 T 2 = S 1 S 3 S 4 . . . S n T_2=S_1S_3S_4...S_n T2=S1S3S4...Sn,又 S 2 < S 1 S_2<S_1 S2<S1,故 T 1 < T 2 T_1<T_2 T1<T2)。
假设数字串前 i − 1 i-1 i−1 位已经构造完成,即数字串前 i − 1 i-1 i−1 个数字是字典序最小的串的开头。此时 S i > S i + 1 S_i >S_{i+1} Si>Si+1,删除第 i i i 位后数字串变为 T 1 = S 1 S 2 . . . S i − 1 S i + 1 S i + 2 . . . S n T_1=S_1S_2...S_{i-1}S_{i+1}S_{i+2}...S_n T1=S1S2...Si−1Si+1Si+2...Sn,删除第 i + 1 i+1 i+1 位后数字串变为 T 2 = S 1 S 2 . . . S i − 1 S i S i + 2 . . . S n T_2=S_1S_2...S_{i-1}S_{i}S_{i+2}...S_n T2=S1S2...Si−1SiSi+2...Sn。由于 T 1 T_1 T1 和 T 2 T_2 T2 的前 i − 1 i-1 i−1 位相同,两串的第 i i i 位为 S i + 1 S_{i+1} Si+1 和 S i S_i Si,故 T 1 < T 2 T_1<T_2 T1<T2。
以上证明可能不太严谨,严谨的证明可百度删数问题。
然后我们只要从左往右找,如果当前数字大于前面的数字,就把前面大于当前的数字都删掉。因为需要找到前面还未被删除的数字,所以拿一个栈或队列来保存和更新答案。
具体做法:遍历数字串,比较栈顶数字(前面还未被删除的数字)和当前数字,弹出栈顶数字直到当前数字大于栈顶数字(或空)。可以发现,最后答案一定是非严格上升的,所以最后若还能继续删除 k k k 数字,删掉最后 k k k 个数字最优。
时间复杂度 O ( n ) O(n) O(n)。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
char s[N];
char stk[N];
void solve(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++) cin>>s[i];
int top=0;
for(int i=1;i<=n;i++){
if(top==0) stk[++top]=s[i];
else{
while(top && stk[top]>s[i] && m){ // 维护非严格上升的数字串
top--;
m--;
}
stk[++top]=s[i];
}
}
while(m--){ // 删除最后m个数字
top--;
}
for(int i=1;i<=top;i++) cout<<stk[i];
cout<<endl;
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int _;cin>>_;
while(_--) solve();
return 0;
}
烦恼的小 h
出题人:周
为了比较好的解决这个问题,下面引入一些在这题特有的符号
- 二元组 ( u , v ) (u,v) (u,v)表示节点 u u u到节点 v v v的异或和
- ∘ \circ ∘ 表示异或符号
下面给出一些显然的性质(不作证明)
- ( u , u ) = 0 (u,u) = 0 (u,u)=0
- ( u , v ) = ( v , u ) (u,v)= (v,u) (u,v)=(v,u)
- ( u , w ) ∘ ( w , v ) = ( u , v ) (u,w)\circ ( w, v) = (u ,v ) (u,w)∘(w,v)=(u,v)
解法一
题意要求我们求出若干组 ( u , v ) (u,v) (u,v)。我们尝试预处理一些东西来使我们更好的计算它们
设 l c a lca lca为它们的最近公共祖先,此时我们有
( u , v ) = ( u , l c a ) ∘ ( l c a , v ) (u,v) = (u , lca)\circ (lca ,v) (u,v)=(u,lca)∘(lca,v)
于是预处理 l c a lca lca 每次找到它们的最近公共祖先后倍增地向上跳即可。时间复杂度 O ( n log n + m log n ) O(n\log n + m\log n) O(nlogn+mlogn)。
参考代码 (下面的代码时间复杂度是 O ( n log 2 n O(n\log^2n O(nlog2n的 也能通过)
#include <bits/stdc++.h>
using namespace std;
#define LL long long
#define endl '\n'
const int maxn = 2e5 + 10;
vector<pair<int, int>> G[maxn];
int anc[maxn][20 + 3];
int val[maxn][20 + 3];
int de[maxn];
void dfs(int u, int fa)
{
for (int i = 1; i <= 20; ++i)
{
val[u][i] = (val[u][i - 1] ^ val[anc[u][i - 1]][i - 1]);
anc[u][i] = anc[anc[u][i - 1]][i - 1];
}
for (auto [w, to] : G[u])
{
if (to == fa)
continue;
de[to] = de[u] + 1;
anc[to][0] = u;
val[to][0] = w;
dfs(to, u);
}
}
int getlca(int x, int y)
{
if (de[x] < de[y])
swap(x, y);
for (int i = 20; i >= 0; i--)
{
if (de[anc[x][i]] >= de[y])
x = anc[x][i];
}
if (x == y)
return x;
for (int i = 20; i >= 0; --i)
{
if (anc[x][i] != anc[y][i])
{
x = anc[x][i];
y = anc[y][i];
}
}
return anc[x][0];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
for (int i = 1; i < n; ++i)
{
int u, v, w;
cin >> u >> v >> w;
G[u].push_back({w, v});
G[v].push_back({w, u});
}
int cnt = 0;
de[1] = 1;
dfs(1, -1);
int q;
cin >> q;
while (q--)
{
int u, v;
cin >> u >> v;
int lca = getlca(u, v);
int ans = 0;
int res1 = 0;
for (int i = 20; i >= 0; --i)
{
if (getlca(anc[u][i], lca) == lca)
{
res1 ^= (val[u][i]);
u = anc[u][i];
}
}
int res2 = 0;
for (int i = 20; i >= 0; --i)
{
if (getlca(anc[v][i], lca) == lca)
{
res2 ^= (val[v][i]);
v = anc[v][i];
}
}
ans = res1 ^ res2;
cout << ans << endl;
}
return 0;
}
解法二
上述的解法并没有很好的利用异或的性质,它仅仅只是利用了异或的结合律。而异或还有一个重要的性质:异或的逆运算是它本身。 而利用这一点,我们可以把时间复杂度降到 O ( n + m ) O(n+m) O(n+m)。
考虑我们需要计算的东西 ( u , v ) (u,v) (u,v)。
我们利用上述的三个性质来推导一些东西
( u , v ) = ( u , l c a ) ∘ ( l c a , v ) ∘ 0 = ( u , l c a ) ∘ ( l c a , v ) ∘ [ ( 1 , l c a ) ∘ ( l c a , 1 ) ] = [ ( u , l c a ) ∘ ( l c a , 1 ) ] ∘ [ ( 1 , l c a ) ∘ ( l c a , v ) ] = ( u , 1 ) ∘ ( 1 , v ) = ( 1 , u ) ∘ ( 1 , v ) \begin{aligned} &( u , v) \\ =&(u,lca) \circ (lca, v)\circ 0 \\ = &(u , lca) \circ ( lca , v ) \circ [(1, lca) \circ ( lca, 1 )]\\ = & [(u,lca) \circ ( lca, 1 ) ] \circ [(1, lca) \circ (lca, v)] \\ = & (u, 1) \circ (1, v )\\ = &( 1, u) \circ (1, v) \end{aligned} =====(u,v)(u,lca)∘(lca,v)∘0(u,lca)∘(lca,v)∘[(1,lca)∘(lca,1)][(u,lca)∘(lca,1)]∘[(1,lca)∘(lca,v)](u,1)∘(1,v)(1,u)∘(1,v)
也就是说,我们只需要计算出 1 1 1到所有节点的异或值。然后询问时,把二者的异或值异或起来就是答案。
参考代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
vector<vector<pair<int, int>>> g(n + 1);
for (int i = 1; i < n; ++i)
{
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({w, v});
g[v].push_back({w, u});
}
vector<int> f(n + 1);
function<void(int, int)> dfs = [&](int u, int fa) -> void
{
for (auto [w, to] : g[u])
{
if (to == fa)
continue;
f[to] = f[u] ^ w;
dfs(to, u);
}
};
dfs(1, -1);
int q;
cin >> q;
while (q--)
{
int u, v;
cin >> u >> v;
cout << (f[u] ^ f[v]) << endl;
}
return 0;
}
难题
烤盐的曾同学
出题人:周
题解太长我不看版:第
n
+
2
n+2
n+2项斐波那契数
−
1
-1
−1 为答案 矩阵快速幂加速即可
设 f ( i ) f(i) f(i)为智慧值为 i i i的鱼的个数,那么也就是说
a n s = ∑ i = 1 n f ( i ) ans = \sum_{i =1 }^{n} f(i) ans=i=1∑nf(i)
其中 f ( i ) f(i) f(i)满足
f ( i ) { 1 if x = 1 1 if x = 2 f ( i − 1 ) + f ( i − 2 ) otherwise f(i)\begin{cases} 1 & \text{ if } x=1 \\ 1 & \text{ if } x= 2\\ f(i-1 ) + f( i -2 ) & \text{otherwise} \end{cases} f(i)⎩ ⎨ ⎧11f(i−1)+f(i−2) if x=1 if x=2otherwise
前两个式子是显然的,而第三个可以这么解释:智慧值为 i i i只能是智慧值为 i − 1 i-1 i−1的左节点和智慧值为 i − 2 i-2 i−2的右节点。显然,他是一个斐波那契数列。
利用这个式子预处理后计算就可以通过 60 % 60\% 60%的测试点。
下面将描述 80 % 80\% 80%的做法和 100 % 100\% 100%的做法。
80 % 80\% 80%的做法
1 0 9 10^9 109在本地需要跑 4 4 4s 左右,我们可以考虑分块打表来加速这一过程
在本地跑一个隔 1 0 6 10^6 106就输出一次的表,每次查询的时候可以快速找到它所在的块 然后剩余的部分暴力跑一遍即可。
时间复杂度为 O ( 能过 ) O(能过) O(能过)
满分做法
分块打表并没有利用上斐波那契的性质,故不能得到满分。
考虑我们要算的东西
a n s = ∑ i = 1 n f ( i ) ans =\sum_{i =1 }^{ n} f(i) ans=i=1∑nf(i)
为了下面的讨论方便,这里设 n n n为大于 2 2 2的奇数(偶数同理)。
将这个东西按奇偶分类,有
a n s = f ( 1 ) + f ( 3 ) + f ( 5 ) + ⋯ + f ( n ) + f ( 2 ) + f ( 4 ) + f ( 6 ) + ⋯ + f ( n − 1 ) = [ f ( 2 ) + f ( 3 ) + f ( 5 ) + ⋯ + f ( n ) ] + [ f ( 1 ) + f ( 2 ) + f ( 4 ) + f ( 6 ) + ⋯ + f ( n − 1 ) ] − f ( 1 ) = f ( n + 1 ) + f ( n ) − f ( 1 ) = f ( n + 2 ) − 1 \begin{aligned} ans =& f(1) + f(3) + f(5) + \cdots + f(n) \\ +&f(2) + f(4) + f(6) + \cdots + f(n -1 ) \\ = & [f(2) + f(3) + f(5) + \cdots + f(n)] \\ +&[f(1) +f(2) + f(4) + f(6) + \cdots + f(n -1 ) ]- f(1) \\ = & f( n +1) + f(n) -f(1) \\ =& f(n +2 ) -1 \end{aligned} ans=+=+==f(1)+f(3)+f(5)+⋯+f(n)f(2)+f(4)+f(6)+⋯+f(n−1)[f(2)+f(3)+f(5)+⋯+f(n)][f(1)+f(2)+f(4)+f(6)+⋯+f(n−1)]−f(1)f(n+1)+f(n)−f(1)f(n+2)−1
单项的斐波那契数列可以用矩阵快速幂在 O ( log n ) O(\log n) O(logn)的时间复杂度内跑出。
#include <bits/stdc++.h>
using namespace std;
#define LL long long
constexpr int P = 998244353;
using i64 = long long;
// assume -P <= x < 2P
int norm(int x)
{
if (x < 0){x += P;}
if (x >= P){x -= P;}
return x;
}
template <class T>
T power(T a, int b)
{
T res = 1;
for (; b; b /= 2, a *= a){if (b % 2){res *= a;}}
return res;
}
struct Z
{
int x;
Z(int x = 0) : x(norm(x)) {}
int val() const{return x;}
Z operator-() const{return Z(norm(P - x));}
Z inv() const{assert(x != 0);return power(*this, P - 2);}
Z &operator*=(const Z &rhs){x = i64(x) * rhs.x % P;return *this;}
Z &operator+=(const Z &rhs){x = norm(x + rhs.x);return *this;}
Z &operator-=(const Z &rhs){x = norm(x - rhs.x);return *this;}
Z &operator/=(const Z &rhs){return *this *= rhs.inv();}
friend Z operator*(const Z &lhs, const Z &rhs){Z res = lhs;res *= rhs;return res;}
friend Z operator+(const Z &lhs, const Z &rhs){Z res = lhs;res += rhs;return res;}
friend Z operator-(const Z &lhs, const Z &rhs){Z res = lhs;res -= rhs;return res;}
friend Z operator/(const Z &lhs, const Z &rhs){Z res = lhs;res /= rhs;return res;}
friend std::istream &operator>>(std::istream &is, Z &a){i64 v;is >> v;a = Z(v);return is;}
friend std::ostream &operator<<(std::ostream &os, const Z &a){return os << a.val();}
};
template <class T>
struct Matrix
{
int siz1, siz2;
vector<vector<T>> v;
Matrix(int n) : siz1(n), siz2(n), v(n, vector<T>(n)){for (int i = 0; i < n; ++i)v[i][i] = T(1);}
Matrix(int n, int m) : siz1(n), siz2(m), v(n, vector<T>(m)) {} //声明两个参数可以不把它化成单位阵
vector<T> &operator[](int i) { return v[i]; }
Matrix operator*(Matrix const &rhs) const
{
assert(rhs.siz1 == siz2);
Matrix<T> ans(siz1, rhs.siz2);
int siz = siz2;
for (int i = 0; i < ans.siz1; ++i)
for (int j = 0; j < ans.siz2; ++j)
for (int k = 0; k < siz; ++k)
{ans.v[i][j] += v[i][k] * rhs.v[k][j];}
return ans;
}
Matrix qpow(long long b) const
{
assert(siz1 == siz2);
Matrix ans(siz1);
Matrix res = *this;
while (b)
{
if (b & 1) ans = ans * res;
res = res * res;
b /= 2;
}
return ans;
}
};
void solve()
{
LL n;
cin >> n;
Matrix<Z> a(2, 2), b(2, 2);
a[0][0] = a[0][1] = a[1][0] = 1;
b[0][0] = b[0][1] = 1;
auto ans = b * a.qpow(n);
cout << ans[0][0] - 1 << endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
cin >> T;
while (T--)
solve();
return 0;
}
死去的2021突然攻击我
出题人:贝
题意
对于给定的正整数 a , b , c , d a,b,c,d a,b,c,d,求满足 x × y x\times y x×y能被 2021 2021 2021整除的数对 ( x , y ) (x,y) (x,y)的数量,其中 a ≤ x ≤ b , c ≤ y ≤ d a\le x \le b, c\le y \le d a≤x≤b,c≤y≤d。
思路
代码实现
算法思路
- 容斥原理(计数问题经常见)
区间之间的容斥
-
我们要求的是
[a,b]
和[c,d]
中满足条件的数对,如图所示
-
计算区间
[1,b]
和[1,d]
满足题目条件的数对个数
-
再减去
[1,a-1]
和[1,d]
中满足的,减去[1,b]
和[1,c-1]
中满足的,
- 容斥原理得,
[1,a-1]
对应[1,c-1]
的数对会被多减去一次,所以要加回来
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mw08BGpa-1664353175545)(C:\Users\GhostLX\AppData\Roaming\Typora\typora-user-images\image-20220922093012343.png)]
- 那么我们最后得到的就是
[a,b]
和[c,d]
中满足题目的数对关系了,因为求解两个区间满足条件的数对关系代码一直反复,所以我们可以写一个函数(思路想清楚再打代码,可以减小代码的冗余量以及bug率)
区间内的容斥:求解数对关系
- 思路进一步推进,我们的问题已经简化为求解
[1,x]
和[1,y]
中满足题目条件的数对个数 - 我们可以发现
2021
可以分解为43*47
,所以问题就被划分成了一下几步
- 此处即
[1,x]
这个区间为A,[1,y]
这个区间为B - A的因子出2021,B的因子出任意 and A的因子出任意,B的因子出2021,即·
x/2021)*y+x*(y/2021)
· - 两边同时出2021的情况会算两次,减去该情况,即
(x/2021)*(y/2021)
- 计算A出47和B出43的情况,此时单出2021的情况已经被计算过了,即
(x/47-x/2021)*(y/43-y/2021)
- 计算A出43,B出47的情况,
(x/43-x/2021)*(y/47-y/2021)
代码实现
#include <bits/stdc++.h>
using namespace std;
// 2021=43*47
typedef long long LL;
LL cal(LL x, LL y)
{ //计算[1,x]和[1,y]中满足题目条件的数对个数
//此处即[1,x]这个区间为A,[1,y]这个区间为B
LL cnt = 0;
cnt += (x / 2021) * y + x * (y / 2021); // A的因子出因子2021,B的因子出任意因子 and A的因子出任意因子,B的因子出因子2021
cnt -= (x / 2021) * (y / 2021); //上式中,两边同时出2021的情况会算两次,此处减去
cnt += (x / 47 - x / 2021) * (y / 43 - y / 2021); //单出2021的情况已经被计算过了,计算A出47和B出43的情况
cnt += (x / 43 - x / 2021) * (y / 47 - y / 2021); //计算A出43,B出47的情况
return cnt;
}
long long findPairs(long long a, long long b, long long c, long long d)
{
return cal(b, d) - cal(a - 1, d) - cal(b, c - 1) + cal(a - 1, c - 1);
//计算区间[1,b]和[1,d]满足题目条件的数对个数
//再减去[1,a-1]和[1,d]中满足的,减去[1,b]中满足的
//容斥原理得,[a-1,c-1]这个区间会被多减去一次,所以要加回来
}
int main()
{
int a, b, c, d;
cin >> a >> b >> c >> d;
cout << findPairs(a, b, c, d);
}
压轴
二分战神
出题人:贝
题意
- 将 a [ ] a[] a[]数组中,任意一个长度大于 k k k的子串的第 k k k大的数放入 b [ ] b[] b[]数组中(不去重),求解 b b b数组中第 m m m大的数
思路
- 尺取法+二分答案
- 二分第 m m m大的数的值 m i d mid mid,如果 b [ ] b[] b[]数组中比 m i d mid mid大的值 < m <m <m则说明 m i d mid mid比答案大需要减小右边界,反之增大
- 那接下来的问题就是如何用尺取法计算 b [ ] b[] b[]数组中比 m i d mid mid大的值
- 定义左边界 i i i和右边界 j j j, s u m sum sum记录当前区间比 m i d mid mid大的数的个数, a n s ans ans记录函数的答案
- 先将长度达到
k
k
k,再继续增加
s
u
m
sum
sum,直至
s
u
m
=
k
sum=k
sum=k,此时后面的所有区间的
[
i
,
k
]
,
k
∈
[
j
+
1
,
n
]
[i,k],k\in [j + 1, n]
[i,k],k∈[j+1,n],取出的第
k
k
k大的数字都
>
m
i
d
>mid
>mid,所以这些数在
b
[
]
b[]
b[]中一定大于
m
i
d
mid
mid,此时
ans += n - j + 1
- 注意枚举右指针时, j < n j<n j<n,且增加 i i i时候,记得根据 a [ i ] a[i] a[i]是否 > m i d >mid >mid来判断 s u m sum sum是否需要减1
代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10, md = 1e9 + 7;
LL a[N], n, k, m;
LL check(int mid) //尺取法判断
{
LL j = 0, sum = 0, ans = 0;
// j代表右指针,sum代表当前区间中大于mid的元素个数
for (int i = 1; i <= n; i++)
{
//如果区间长度<k,并且下标还没有越界
while (j - i + 1 < k && j < n)
{
j++;
if (a[j] > mid)
sum++; //比 mid大的数加一
}
while (sum < k && j < n) //区间长度从k继续增加
{
j++;
if (a[j] > mid)
sum++;
}
if (sum < k) //说明j==n的时候sum都<k
break;//则接下来没有任何一个区间会有贡献
ans += (n - j + 1); //[i,k], 其中k in [j + 1, n]
//这些区间的k th 都必然 > mid
if (a[i] > mid) //下一个区间不包括这个i位置
sum--;
}
return ans; //比mid大的b数组元素个数
}
int main()
{
cin.tie(0);
cout.tie(0), cin.sync_with_stdio(false);
int T;
cin >> T;
while (T--)
{
cin >> n >> k >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
int str = 0, end = 1e9;
while (str <= end) //二分答案
{
int mid = (str + end) / 2;
if (check(mid) < m) //长度大于k的区间中,比mid大元素的数量<m
{//这样的方案才合法,才有可能是答案
end = mid - 1;
}
else
{
str = mid + 1;
}
}
cout << str << endl;
}
}
我回来了
出题人:吴
题意
给你一个序列 a a a 让你支持
-
[ l , r ] [l,r] [l,r] 区间赋值
-
[ l , r ] [l,r] [l,r] 询问区间最小值
但我们不会直接给你序列 a a a,而是给你序列一个长度为 n n n 的序列 b b b ,把 b b b 复制粘贴 k k k 次就可以得到 a a a。
思路
值域 1 e 9 1e9 1e9,考虑线段树动态开点,但是初始化朴树建树时间复杂度为 O ( n k ∗ l o g 2 ( n k ) ) O(nk*log_2(nk)) O(nk∗log2(nk)) 会T。
假设原数组从 0 0 0 开始。
首先线段树里需要维护区间最小值,考虑开点时如何初始化一个节点的区间最小值(区间 [ l , r ] [l,r] [l,r] ):
- 若 r − l + 1 > = n r-l+1>=n r−l+1>=n,那么区间最小值一定是 b b b 数组的最小值。
- 若 r − l + 1 < n r-l+1<n r−l+1<n,且 ⌊ l n ⌋ = ⌊ r n ⌋ \lfloor \frac{l}{n} \rfloor = \lfloor \frac{r}{n} \rfloor ⌊nl⌋=⌊nr⌋,即 [ l % n , r % n ] [l\%n,r\%n] [l%n,r%n] 是 b b b 中的一段,此时主要查找 b b b 数组中 [ l % n , r % n ] [l\%n,r\%n] [l%n,r%n]的最小值。
- 若 r − l + 1 < n r-l+1<n r−l+1<n,且 ⌊ l n ⌋ ≠ ⌊ r n ⌋ \lfloor \frac{l}{n} \rfloor \ne \lfloor \frac{r}{n} \rfloor ⌊nl⌋=⌊nr⌋,那么需要取 [ l % n , n − 1 ] [l\%n,n-1] [l%n,n−1] 和 [ 0 , r % n ] [0,r\%n] [0,r%n] 的最小值。
- 上述查询可以对 b b b 数组建立ST表或线段树完成查询。(因为每次开点的时候都是在原序列的基础上获取信息)
剩下的就是动态开点板子了。
使用ST表时间复杂度为 O ( n ∗ l o g 2 n + q ∗ l o g 2 ( n ∗ k ) ) O(n*log_2n+q*log_2(n*k)) O(n∗log2n+q∗log2(n∗k))。
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5;
int n,k;
int a[N];
namespace ST{
int logn[N];
void pre(){
logn[1]=0,logn[2]=1;
for(int i=3;i<N;i++) logn[i]=logn[i/2]+1;
}
int st[N][21];
void init(int n){
pre();
for(int i=0;i<n;i++) st[i][0]=a[i];
for(int j=1;j<=20;j++){
for(int i=0;i+(1<<j)-1<n;i++){
st[i][j]=min(st[i][j-1], st[i+(1<<(j-1))][j-1]);
}
}
}
int query_min(int l,int r){
int k=logn[r-l+1];
return min(st[l][k], st[r-(1<<k)+1][k]);
}
};
namespace Segment_Tree{
const int inf=1e9+10;
int tot=0;
struct node{
int ls,rs;
int minn;
int same;
}tr[N*55];
void make_node(int& x,int l,int r){
if(x) return;
x=++tot;
tr[x].same=-1;
if(r-l+1>=n) tr[x].minn=ST::query_min(0,n-1);
else if(l/n == r/n) tr[x].minn=ST::query_min(l%n, r%n);
else tr[x].minn=min(ST::query_min(0,r%n), ST::query_min(l%n,n-1));
}
void do_same(int& u,int ul,int ur,int x){
tr[u].minn=x;
tr[u].same=x;
}
void pushdown(int& u,int ul,int ur){
int mid=ul+ur>>1;
make_node(tr[u].ls,ul,mid);
make_node(tr[u].rs,mid+1,ur);
if(~tr[u].same){
do_same(tr[u].ls,ul,mid,tr[u].same);
do_same(tr[u].rs,mid+1,ur,tr[u].same);
tr[u].same=-1;
}
}
void pushup(int& u){
tr[u].minn=min(tr[tr[u].ls].minn, tr[tr[u].rs].minn);
}
void update_same(int& u,int ul,int ur,int l,int r,int x){
make_node(u,ul,ur);
if(l<=ul && ur<=r){
do_same(u,ul,ur,x);
return;
}
pushdown(u,ul,ur);
int mid=ul+ur>>1;
if(l<=mid) update_same(tr[u].ls, ul, mid, l, r, x);
if(mid<r) update_same(tr[u].rs, mid+1, ur, l, r, x);
pushup(u);
}
int query_min(int& u,int ul,int ur,int l,int r){
make_node(u,ul,ur);
if(l<=ul && ur<=r) return tr[u].minn;
pushdown(u,ul,ur);
int mid=ul+ur>>1;
int res=inf;
if(l<=mid) res=min(res, query_min(tr[u].ls, ul, mid, l, r));
if(mid<r) res=min(res, query_min(tr[u].rs, mid+1, ur, l, r));
return res;
}
};
void solve(){
cin>>n>>k;
for(int i=0;i<n;i++) cin>>a[i];
ST::init(n);
int root=0;
Segment_Tree::make_node(root,0,n-1);
int q;cin>>q;
while(q--){
int op;cin>>op;
if(op==1){
int l,r,x;cin>>l>>r>>x;
l--;r--;
Segment_Tree::update_same(root,0,n*k-1,l,r,x);
}
else{
int l,r;cin>>l>>r;
l--;r--;
cout<<Segment_Tree::query_min(root,0,n*k-1,l,r)<<endl;
}
}
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
solve();
return 0;
}
另外本题写ODT可以拿到 80 % 80\% 80% 的分数(因为实在是卡不动了悲)。
验题人题解
贝
建立三类线段树:
- 原始数据,区间长度为N
- 每个复制出来的块,建立一棵线段树,区间长度为K
- 块中具体的每个位置,动态开点建立线段树
于是我花了2h
写出了400
行的代码才过了这题(出题人过于毒瘤)
#include <bits/stdc++.h>
using namespace std;
#define debug(x) cerr << #x << ": " << x << '\n';
#define bd cerr << "----------------------" << el;
#define el '\n'
#define cl putchar('\n');
#define pb push_back
#define eb emplace_back
#define x first
#define y second
#define rep(i, a, b) for (int i = (a); i <= (b); i++)
#define lop(i, a, b) for (int i = (a); i < (b); i++)
#define dwn(i, a, b) for (int i = (a); i >= (b); i--)
#define ceil(a, b) (a + (b - 1)) / b
#define ms(a, x) memset(a, x, sizeof(a))
#define INF 0x3f3f3f3f
#define db double
#define all(x) x.begin(), x.end()
#define reps(i, x) for (int i = 0; i < x.size(); i++)
#define cmax(a, b) a = max(a, b)
#define cmin(a, b) a = min(a, b)
#define mpa make_pair
typedef long long LL;
typedef long double LD;
typedef pair<int, int> PII;
typedef pair<db, db> PDD;
typedef vector<int> vci;
struct read
{
static const int M = 1 << 21;
char buf[M], *S = buf, *P = buf, c, f;
inline char getc()
{
return (S == P && (P = (S = buf) + fread(buf, 1, 1 << 21, stdin), S == P) ? EOF : *S++);
}
template <typename T>
read &operator>>(T &x)
{
for (c = 0; !isdigit(c); c = getc())
f = c;
for (x = 0; isdigit(c); c = getc())
x = x * 10 + (c - '0');
return x = (f ^ '-') ? x : -x, *this;
}
} fin;
constexpr int N = 1e5 + 10, M = 4 * N, B = 66, md = 1e9 + 7;
const double PI = acos(-1), eps = 1e-8;
int T, n, m, k;
int b[N];
int initMi, initMx;
struct NodeS
{
int mi;
int tag;
} seg[M], s[M];
//原线段树
void buildS(int u, int l, int r)
{
s[u].tag = -1;
if (l == r)
{
s[u].mi = b[l];
return;
}
int mid = l + r >> 1;
buildS(u << 1, l, mid);
buildS(u << 1 | 1, mid + 1, r);
s[u].mi = min(s[u << 1].mi, s[u << 1 | 1].mi);
}
#define lson(u) (u) << 1, l, mid
#define rson(u) (u) << 1 | 1, mid + 1, r
int queryS(int u, int l, int r, int ql, int qr)
{
if (ql <= l && r <= qr)
{
return s[u].mi;
}
int mid = l + r >> 1;
if (qr <= mid) //左半边
return queryS(lson(u), ql, qr);
else if (ql > mid) //右半边
return queryS(rson(u), ql, qr);
else
{
int lans = queryS(lson(u), ql, qr);
int rans = queryS(rson(u), ql, qr);
return min(lans, rans);
}
}
int queryS(int ql, int qr)
{
return queryS(1, 1, n, ql, qr);
}
//动态开点 线段树
inline PII getKuai(int i)
{ // x块的
return mpa((i - 1) * n + 1, i * n);
}
inline int getL(int i)
{
return (i - 1) * n + 1;
}
inline int getR(int i)
{
return i * n;
}
inline int getK(int l)
{
return (l - 1) / n + 1;
}
// [(i - 1)n + 1, i n]
struct Node
{
int L, R; //左右指针
int mi;
int tag;
int su; //对应的原始线段树的节点
} dy[N * 64];
int tot;
int kToDy[N * 4];
void buildDy(int u)
{
dy[u] = {-1, -1, initMi, -1, 1};
}
void pushDownDy(int u, int son)
{
if (dy[u].tag != -1)
{
dy[son].tag = dy[u].tag;
dy[son].mi = dy[u].tag;
}
}
void pushUpDy(int u)
{
int tmp1, tmp2;
if (~dy[u].L)
tmp1 = dy[dy[u].L].mi;
else {
// if(~dy[u].tag)
// tmp1 = dy[u].tag;
// else
tmp1 = s[dy[u].su << 1].mi;
}
if (~dy[u].R)
tmp2 = dy[dy[u].R].mi;
else {
// if(~dy[u].tag)
// tmp2 = dy[u].tag;
// else
tmp2 = s[dy[u].su << 1 | 1].mi;
}
dy[u].mi = min(tmp1, tmp2);
}
int queryDy(int u, int l, int r, int ql, int qr)
{ //查询的一定不是完整的
if (~dy[u].tag) //存在tag,则说明整个区间全是一个数
//此时查询区间交集的最小值,也一定是这个值
return dy[u].mi;
if (ql <= l && r <= qr)
{
return dy[u].mi;
}
int mid = l + r >> 1;
int tmp = INT_MAX;
if (ql <= mid)
{
if (!~dy[u].L)
{ //不存在左儿子的时候,访问原本的子树
cmin(tmp, queryS(dy[u].su << 1, l, mid, ql, qr));
}
else
{
pushDownDy(u, dy[u].L);
cmin(tmp, queryDy(dy[u].L, l, mid, ql, qr));
}
}
if (qr > mid)
{
if (!~dy[u].R)
{ //不存在右儿子的时候
cmin(tmp, queryS(dy[u].su << 1 | 1, mid + 1, r, ql, qr));
}
else
{
pushDownDy(u, dy[u].R);
cmin(tmp, queryDy(dy[u].R, mid + 1, r, ql, qr));
}
}
if(~dy[u].L && ~dy[u].R)
dy[u].tag = -1; // tag一定下传完毕
return tmp;
}
int queryDy(int u, int ql, int qr)
{
return queryDy(u, 1, n, ql, qr);
}
void mapSToDy(int u, int su)
{ //将s[su]的节点信息赋值给dy[u]
dy[u].su = su;
dy[u].mi = s[su].mi;
dy[u].tag = -1;
dy[u].L = dy[u].R = -1;
}
void modifyDy(int u, int l, int r, int ml, int mr, int x)
{
if (ml <= l && r <= mr)
{
dy[u].mi = dy[u].tag = x;
return;
}
int mid = l + r >> 1;
if(~dy[u].tag)
{
if (!~dy[u].L)
{ //不存在左儿子的时候
int L = dy[u].L = ++tot;
mapSToDy(L, dy[u].su << 1);
}
if (!~dy[u].R)
{ //不存在左儿子的时候
int R = dy[u].R = ++tot;
mapSToDy(R, dy[u].su << 1 | 1);
}
pushDownDy(u, dy[u].L);
pushDownDy(u, dy[u].R);
dy[u].tag = -1;
}
if (ml <= mid)
{
if (!~dy[u].L)
{ //不存在左儿子的时候
int L = dy[u].L = ++tot;
mapSToDy(L, dy[u].su << 1);
}
modifyDy(dy[u].L, l, mid, ml, mr, x);
}
if (mr > mid)
{
if (!~dy[u].R)
{ //不存在左儿子的时候
int R = dy[u].R = ++tot;
mapSToDy(R, dy[u].su << 1 | 1);
}
modifyDy(dy[u].R, mid + 1, r, ml, mr, x);
}
pushUpDy(u); //修改的话,需要pushUP
if(~dy[u].L && ~dy[u].R)
dy[u].tag = -1; // tag一定下传完毕
}
//分块 线段树
void buildSeg(int u, int l, int r)
{
seg[u].tag = -1;
if (l == r)
{
seg[u].mi = initMi;
kToDy[l] = ++tot;
buildDy(kToDy[l]);
return;
}
int mid = l + r >> 1;
buildSeg(u << 1, l, mid);
buildSeg(u << 1 | 1, mid + 1, r);
seg[u].mi = min(seg[u << 1].mi, seg[u << 1 | 1].mi);
}
PII intersection(int l1, int r1, int l2, int r2)
{
return mpa(max(l1, l2), min(r1, r2));
}
int toMod(int x)
{
return (x - 1) % n + 1;
}
void pushUpSeg(int u)
{
seg[u].mi = min(seg[u << 1].mi, seg[u << 1 | 1].mi);
}
void pushDownSeg(int u)
{
if (~seg[u].tag)
{
seg[u << 1].tag = seg[u << 1].mi = seg[u].tag;
seg[u << 1 | 1].tag = seg[u << 1 | 1].mi = seg[u].tag;
seg[u].tag = -1;
}
}
int querySeg(int u, int l, int r, int ql, int qr, int kl, int kr)
{
if (l == r)
{
PII k = getKuai(l);
if (ql <= k.x && k.y <= qr)
return seg[u].mi;
PII jiao = intersection(k.x, k.y, ql, qr);
if (~seg[u].tag)
{
dy[kToDy[l]].mi = dy[kToDy[l]].tag = seg[u].tag;
seg[u].tag = -1;
}
return queryDy(kToDy[l], toMod(jiao.x), toMod(jiao.y));
}
int kx = getL(l);
int ky = getR(r);
if (ql <= kx && ky <= qr)
return seg[u].mi;
pushDownSeg(u);
int mid = l + r >> 1;
if (kr <= mid) //左半边
return querySeg(lson(u), ql, qr, kl, kr);
else if (kl > mid) //右半边
return querySeg(rson(u), ql, qr, kl, kr);
else
{
int lans = querySeg(lson(u), ql, qr, kl, kr);
int rans = querySeg(rson(u), ql, qr, kl, kr);
return min(lans, rans);
}
}
int querySeg(int ql, int qr)
{
int kl = getK(ql);
int kr = getK(qr);
return querySeg(1, 1, k, ql, qr, kl, kr);
}
void modifySeg(int u, int l, int r, int ml, int mr, int x, int kl, int kr)
{
if (l == r)
{
PII k = getKuai(l);
PII jiao = intersection(k.x, k.y, ml, mr);
if (~seg[u].tag)
{
dy[kToDy[l]].mi = dy[kToDy[l]].tag = seg[u].tag;
seg[u].tag = -1;
}
modifyDy(kToDy[l], 1, n, toMod(jiao.x), toMod(jiao.y), x);
seg[u].mi = dy[kToDy[l]].mi;
return;
}
int kx = getL(l);
int ky = getR(r);
if (ml <= kx && ky <= mr)
{
seg[u].mi = seg[u].tag = x;
return;
}
int mid = l + r >> 1;
pushDownSeg(u);
if (kl <= mid) //左半边
modifySeg(lson(u), ml, mr, x, kl, kr);
if (kr > mid) //右半边
modifySeg(rson(u), ml, mr, x, kl, kr);
pushUpSeg(u);
}
void modifySeg(int ml, int mr, int x)
{
int kl = getK(ml);
int kr = getK(mr);
modifySeg(1, 1, k, ml, mr, x, kl, kr);
}
// #define fin cin
int main()
{
fin >> n >> k;
rep(i, 1, n)
{
fin >> b[i];
}
//建立原始的线段树
buildS(1, 1, n);
initMi = queryS(1, n); //初始最小值
buildSeg(1, 1, k);
int op, l, r, x;
// rep(i, 1, 2 * n - 1)
// {
// cerr << i << el;
// cerr << s[i].mi << ' ' << el;
// bd;
// }
fin >> T;
while (T--)
{
fin >> op;
if (op == 1)
{
fin >> l >> r >> x;
modifySeg(l, r, x);
}
else
{
fin >> l >> r;
cout << querySeg(l, r) << el;
}
// debug(op);
}
// cout << queryS(1, n, 0) << ' ' << queryS(1, 1, 1) << ' ' << queryS(1, n, 1);
}