2022暑期杭电第九场

本文深入探讨了四种不同的算法问题,包括快速冒泡排序的优化策略,通过线性动态规划解决俄罗斯套娃问题,寻找最短路径的GCD图算法,以及数组操作中求期望值的方法。文章详细分析了每种问题的思路,给出了相应的代码实现,并揭示了算法背后的数学原理。通过对这些算法的理解,有助于提升在数据结构和算法领域的实践能力。
摘要由CSDN通过智能技术生成

1003题

Fast Bubble Sort

原题链接

题目大意

定义 B ( A ) B(A) B(A)是对序列 A A A进行一次冒泡排序

冒泡排序

可以对序列进行区间左右移动 1 1 1 个单位长度(数组区间循环移位)
现在给出一个序列,对任意区间提问,求有最小可以用几次上述操作就可以做到使 P [ l . . . r ] P[l...r] P[l...r] 变为 B ( P [ l . . . r ] ) B(P[l...r]) B(P[l...r])

思路

在样例中:
P ( 3 , 7 , 9 , 2 , 6 , 4 , 5 , 8 , 10 , 1 ) = 3 , 7 , 2 , 6 , 4 , 5 , 8 , 9 , 1 , 10 P(3,7,9,2,6,4,5,8,10,1)=3,7,2,6,4,5,8,9,1,10 P(3,7,9,2,6,4,5,8,10,1)=3,7,2,6,4,5,8,9,1,10
我们发现有很大一块是在左右两侧的序列中是相同的,这一段是 2 , 6 , 4 , 5 , 8 2,6,4,5,8 2,6,4,5,8
我们观察到左侧的序列变为右侧的序列时,这一段是向左移动了一位,然后 9 9 9就会放在这一段序列后面。
那看上去我们把数组划分成若干段,每一段都是数组向左移动一位:
3 ∣ 7 ∣ 9 , 2 , 6 , 4 , 5 , 8 ∣ 10 , 1 3|7|9,2,6,4,5,8|10,1 3∣7∣9,2,6,4,5,8∣10,1

发现

  1. 每一段的开头组成了以第一个数 3 3 3为起始点的最长上升子序列
  2. 并且我们发现每一段最开始的节点会被换到这一段的最后面,例如 9 9 9换到了 8 8 8 后,也就是这一段序列左移一位(如果这一段长度为一,那可以不用移动)
  3. 每一段的最大值是这一段的起始值,且要小于下一段的起始值

考虑到数据量比较大,这时候一般就开始思考

  1. 如何才能合并区间答案

  2. 如何才能做到把一个点添加到这段序列并且同时维护答案

发现 1 1 1 操作需要考虑太多情况,每段区间所需要记忆的变量太多了,所以排除 1 1 1 方案
于是开始思考 2 2 2 方案:
先从左往右考虑,如果在序列的右侧添加一个新的值,则会导致每一个左端点到当前点划分的情况都不相同,所以还是很难。
换思路:如果我们是从左侧添加一个新的值,就会发现从左端点到右侧任意一个端点划分的情况只是相当于把右端点更远的划分情况截断了一部分,即对于这两个序列共有的前缀,他们有一个共同的划分方法。

那或许我们应当从右到左的思考:
我们想这个数组是如何划分出来的:

  1. 如果有一个序列,左侧希望添加一个值,这个值比右边的数小,则这个数单独看作一段
  2. 否则,就会把右侧所有比他小的值与这个值一起划分成一段

把每一段的第一个值抽出来,很明显就是一个单调栈,维护了最大值(在样例中为 3 , 7 , 9 , 10 3,7,9,10 3,7,9,10 )

既然单调栈都出来了,且查询量为 1 e 5 1e5 1e5,那我们就直接不演了,直接开始考虑线段树(等)+单调栈
(这种方法主要思想:从 n n n 1 1 1 依次扫描左端点 l l l,维护每个右端点上的答案 a n s R [ ] ansR[] ansR[],则当 l l l q l ql ql 时,如果查询 [ q l , q r ] [ql,qr] [ql,qr] 的答案,只需要查询 a n s R [ q r ] ansR[qr] ansR[qr] 上的值即可,也就是说,这种方法需要离线询问。虽然其实在线询问也可以,上述操作可持久化就行)
(这样想的原理是:单调栈上每个元素都可以对应到一段区间上去,可以把单调栈的 p u s h push push p o p pop pop 操作与线段树的区间修改操作绑定,同步进行,如果 p u s h push push 就添加标记,如果 p o p pop pop 就收回标记)
我们思考添加的这个值会对右侧所有答案造成什么影响(也就是左边界 i i i 变为 i − 1 i-1 i1 时,所有右边界上的答案 a n s R [ ] ansR[] ansR[] 会怎么变)

懒得思考,打个暴力先:

int q;
cin >> n >> q;
rep(i, 1, n) cin >> p[i];
for (int i = n; i; --i) {
    cout << "L:" << i << "\tansR[]:{ ";
    for (int j = 1; j <= n; ++j) {
        if (j < i) {
            cout << "0 ";
            continue;
        }
        int t = p[i], ans = 0, flag = 1;
        for (int k = i + 1; k <= j; ++k) {
            if (p[k] > t)
                t = p[k], flag = 1;
            else if (flag)
                ++ans, flag = 0;
        }
        cout << ans << " ";
    }
    cout << "}";
    cout << endl;
}
/*
输入:
10 0
3 7 9 2 6 4 5 8 10 1
输出:
L:10    ansR[]:{ 0 0 0 0 0 0 0 0 0 0 }
L:9     ansR[]:{ 0 0 0 0 0 0 0 0 0 1 }
L:8     ansR[]:{ 0 0 0 0 0 0 0 0 0 1 }
L:7     ansR[]:{ 0 0 0 0 0 0 0 0 0 1 }
L:6     ansR[]:{ 0 0 0 0 0 0 0 0 0 1 }
L:5     ansR[]:{ 0 0 0 0 0 1 1 1 1 2 }
L:4     ansR[]:{ 0 0 0 0 0 1 1 1 1 2 }
L:3     ansR[]:{ 0 0 0 1 1 1 1 1 1 2 }
L:2     ansR[]:{ 0 0 0 1 1 1 1 1 1 2 }
L:1     ansR[]:{ 0 0 0 1 1 1 1 1 1 2 }
*/

我们发现

  1. 如果新添加的这个点所在区间长度并不是 1 1 1,则从这个点往后所有点的答案都会加一

  2. 否则,所有 a n s R [ ] ansR[] ansR[] 都不会变动

于是我们便可以在 p u s h push push单调栈的时候同时操作一下树状数组,把 i + 1 i+1 i+1 n n n 的位置上的 a n s R [ ] ansR[] ansR[] 都加一
然后 p o p pop pop 单调栈时做反向操作,撤回对应区间上的贡献 1 1 1 即可
(树状数组既然要区间修改,单点查询,那就用树状数组维护前缀和就行)
这样做的正确性在于:

  1. 如果当前这一位添加到原有的序列,且这一位所在的段只有他自己,那所有包含他的区间答案数并不会改变,因为这一位不需要区间左右移动
  2. 否则,就会把一些靠左的区间删除掉,删除后,以当前这个值作为开头的那一段上除去这个值所有的答案都会加一,因为他们既然在一个区间内,那就需要把当前值换到这一段的末尾才行。如: 9 , 2 , 6 , 4 , 5 9,2,6,4,5 9,2,6,4,5, 9 , 2 , 6 , 4 9,2,6,4 9,2,6,4都需要把 9 9 9 挪到最后面,所以 5 5 5, 4 4 4 所在位置上的 a n s R ansR ansR 都会增加 1 1 1
    然后其他的区间段虽然他们自己都不用移动,或者移动次数已经算在答案里了,但是他们的答案内还缺一个把当前这个值移动到所在段最后一个位置所消耗的 1 1 1 的代价。综上所述,这种情况下需要对当前这一位的下一位开始所有的位置上答案数 a n s R ansR ansR + 1 +1 +1

代码

#include <bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for (int i = a, i##end = b; i <= i##end; ++i)
#define per(i, b, a) for (int i = a, i##end = b; i >= i##end; --i)
typedef long long LL;
#define endl '\n'
const int INF = 1e9 + 7;
const int LIM = 1e5 + 10;
typedef pair<int, int> pii;
template <class T>
class fenwick {
    vector<T> c;
    int n;

   public:
    fenwick(int n) : c(n + 1), n(n) {}
    void add(int x, T val) {
        assert(0 < x);
        // assert(x <= n);
        for (; x <= n; x += x & -x) c[x] += val;
    }
    T sum(int x) {
        // assert(0 <= x && x <= n);
        T ans = 0;
        for (; x; x -= x & -x) ans += c[x];
        return ans;
    }
};
int p[LIM], n;
vector<pii> Q[LIM];
int ans[LIM];
void solve() {
    int q;
    cin >> n >> q;
    rep(i, 1, n) cin >> p[i];
    rep(i, 1, n) Q[i].clear();
    rep(i, 1, q) {
        int l, r;
        cin >> l >> r;
        Q[l].push_back({r, i});
    }
    stack<pii> MAX;
    fenwick<int> tmp(n); // 就是ansR[]
    MAX.push({INF, n + 1});
    for (int i = n; i; --i) {
        while (MAX.top().first < p[i]) {
            int l = MAX.top().second, r;
            MAX.pop();
            r = MAX.top().second;
            if (l + 1 == r) continue;
            tmp.add(l + 1, -1); // ansR[l+1...n]--(撤回标记)
        }
        if (i + 1 != MAX.top().second) tmp.add(i + 1, 1); // ansR[i+1...n]++
        MAX.push({p[i], i});
        for (auto p : Q[i]) ans[p.second] = tmp.sum(p.first);
    }
    for (int i = 1; i <= q; ++i) cout << ans[i] << endl;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    rep(i, 1, t) solve();
} 
 

好像有树上倍增的解法

1007题

Matryoshka Doll——线性DP

原题链接

题目大意

T T T 组样例,每组给 n 、 k 、 r n、k、r nkr 三个数和一个大小为 n n n 的升序数组,要求将数组分为 k k k 组,每组都是升序且相邻两元素差值不能小于 r r r

思路

考虑DP,设f[i][j]=前i个数分成j组时的情况数
例:

4 3 3
1 2 3 4 5 6 7
1 1 1 1 1 1 2 2 2 2 
分2组
1 3 | 2 4
f[4][2]=1
分3组
1 3 | 2 | 4
1 4 | 2 | 3
2 4 | 1 | 3
f[4][3]=3
分4组
1 | 2 | 3 | 4
f[4][4]=1 

观察得i扩展时元素a[i]有两种决策:

  1. 单独成组
  2. 和前面的成组

决策1无限制, f [ i ] [ j ] + = f [ i − 1 ] [ j − 1 ] f[i][j] += f[i-1][j-1] f[i][j]+=f[i1][j1]
决策2显然受到 r r r 的限制,观察前 i − 1 i-1 i1 个数的分组情况,发现前 i − 1 i-1 i1 个元素的所有合法答案中,和i不相容的元素必然单独分列在一些组的末尾,没有在同一组的情况。这是因为这些元素受r的限制同时又和i冲突,则其值有( a [ i ] − r , a [ i ] a[i]-r,a[i] a[i]r,a[i] ]的范围限制,故两两冲突不能在同一组。
去除不相容的组后,元素 a [ i ] a[i] a[i] 可以接在任一组后, f [ i ] [ j ] + = f [ i − 1 ] [ j ] ∗ ( j − ( a [ 0 ]   a [ i − 1 ] f[i][j] += f[i-1][j]*(j-(a[0]~a[i-1] f[i][j]+=f[i1][j](j(a[0] a[i1] 中和 a [ i ] a[i] a[i] 冲突的部分));

初值: f [ 0 ] [ 0 ] = 1 初值:f[0][0]=1 初值:f[0][0]=1
最终值: f [ n ] [ k ] = n 个数分成 k 组的情况数 最终值:f[n][k]=n个数分成k组的情况数 最终值:f[n][k]=n个数分成k组的情况数
转移方程:
f [ i ] [ j ] + = f [ i − 1 ] [ j − 1 ] ∗ 1 f[i][j] += f[i-1][j-1]*1 f[i][j]+=f[i1][j1]1
f [ i ] [ j ] + = f [ i − 1 ] [ j ] ∗ ( j − ( a [ 0 ]   a [ i − 1 ] 中和 a [ i ] 冲突的部分 ) ) ; f[i][j] += f[i-1][j]*(j-(a[0]~a[i-1]中和a[i]冲突的部分)); f[i][j]+=f[i1][j](j(a[0] a[i1]中和a[i]冲突的部分));
伪代码:

f[0][0]=1; 
for i in 1...n
f[i][j] = f[i-1][j-1];
f[i][j] +=f[i-1][j]*(j-fal[i]);
f[i][j] %=mod;

优化:
如果理解了上面的转移方程,就会发现转移只和 j / j − 1 j/j-1 j/j1有关,于是可以将 f [ i ] f[i] f[i] 这一维省略,优化空间。注意 f [ 0 ] f[0] f[0] 此时不能设为 1 1 1,因为只有 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1,于是需要稍微处理一下,发现 1 1 1 个元素时只有分 1 1 1 组的 1 1 1 种情况,故:
初值: f [ 1 ] = 1 ( f [ 1 ] [ 1 ] = 1 ) 初值:f[1]=1 (f[1][1]=1) 初值:f[1]=1f[1][1]=1)
最终值: f [ k ] ( f [ n ] [ k ] ) 最终值:f[k] (f[n][k]) 最终值:f[k](f[n][k])
伪代码:

for j in 2...n
f[j] *= (j-fal[i]);
f[j] += f[j-1];
f[j] %=mod;

代码

#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
inline int min(int a,int b)
{
	return a<b?a:b;
}
const int mod=998244353;
int arr[5005],fal[5005];
ll f[5005];
int main()
{
	int t; scanf("%d",&t);
	while(t--)
	{
		int n,k,r;
		scanf("%d%d%d",&n,&k,&r);
		memset(f,0,sizeof(*f)*(n+1)); 
		for(int i=1;i<=n;i++)
		{
			scanf("%d",arr+i);
		}
		for(int i=1,p=1;i<=n;i++)
		{
			while(arr[i]-arr[p]>=r)p++;
			fal[i]=i-p;
		}
		f[1]=1;
		for(int i=2;i<=n;i++)
		{
			for(int j=min(i,k);j;j--)
			{
				f[j] *= (j-fal[i]);
				f[j] += f[j-1];
				f[j] %=mod;
			}
		}
		printf("%lld\n",f[k]);
	}
	return 0;
}

1008题

Shortest Path in GCD Graph

题目链接

题目大意:

给出 n n n 个点,每个点的值为 1 − n 1-n 1n 且不重复,任意两个点都有一条边,边权为两个点值的 $gcd#

每次询问会给出两个点,要求你求出这两点间的最短距离以及最短距离路径的个数

思路:

询问时,若询问的两个点互质,则为 x − > y x->y x>y 为最短路,长度为 1 1 1

若询问的两个点不互质,则 x x x y y y 最短长度为 2 2 2,因为总有 x − > 1 − > y = = 2 x->1->y==2 x>1>y==2,所以只要找到所有与 x , y x,y xy同时互质的数的个数

分别求两个数的质因数并去重,利用容斥原理求出能整除这些质因数的数量,再用 n n n 减去,便得出答案

注意,若 x x x y y y g c d gcd gcd 2 2 2,则答案需要加一

代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#include<set>
#include<fstream>
#define int long long
using namespace std;
const int N = 1e7 + 10;
const int mod = 998244353;
int prime[N],f[N],ans;
int n, q;
bool isprime[N];
set<int> s;
vector<int> a;
void init(int n)
{
	isprime[1] = 1;
	int cnt = 0;
	for (int i = 2; i <= n; ++i)
	{
		if (!isprime[i])
		{
			prime[++cnt] = i;
			f[i] = i;
		}
		for (int j = 1; j <= cnt && prime[j] <= n / i; ++j)
		{
			isprime[prime[j] * i] = 1;
			f[prime[j] * i] = prime[j];
			if (i%prime[j] == 0) break;
		}
	}
}
int gcd(int a, int b)
{
	return b ? gcd(b, a%b) : a;
}
void zhi(int x)
{
	while (x > 1)
	{
		int t = f[x];
		if (!s.count(t)) s.insert(t);
		while (!(x%t)) x /= t;
	}
}
void dfs(int x, int s, int p,int len)
{
	if (x == len)
	{
		ans += p * (n/s);
		ans %= mod;
		return;
	}
	dfs(x + 1, s, p, len);
	if (s <= n / a[x]) dfs(x + 1, s*a[x], -p, len);
}
void suan(int x, int y,int n)
{
	s.clear(),a.clear(),ans=0;
	zhi(x),zhi(y);
	int len = s.size();
	ans = 0;
	for (auto i : s) a.push_back(i);
	dfs(0, 1, 1,len);
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> q;
	init(n);
	for (int i = 1; i <= q; ++i)
	{
		int x, y;
		cin >> x >> y;
		int g = gcd(x, y);
		if (g == 1) {
			cout << 1 << ' ' << 1 << endl;
		}
		else
		{
			suan(x, y,n);
			if (g == 2) ans++;
			cout << 2 << ' ' << ans << endl;
		}
	}
	return 0;
}

1010题

Sum Plus Product

题目链接

题目大意

给定一个长度为 n n n 的数组,每次随机拿出两个数使其变成 ( a + b + a ∗ b ) (a + b + a * b) (a+b+ab)再放回数组,最终数组中只剩下一个数,求剩余数字的期望是多少。

思路

先探索一下三个球的情况
_ABCO_6_131YYT4Z_O_6QXX.png

发现经过操作的两个球得到的答案具有“轮换性”——任意交换两个字母,多项式不变,这就意味着交换两个球的操作次序,答案是唯一的,因此我们产生最终剩余的球的数字与先操作后操作哪个球无关的猜测。但是我们这里只做了 a a a b b b, a b ab ab c c c, a b c abc abc d d d 这样的操作,但还有 a a a b b b , c c c d d d , a b ab ab c d cd cd这样的方式我们没有讨论到,下面做出粗糙的证明。

大概证明:

①首先我们知道包含n个字母的多项式最多有的项数是:
C n 1 + C n 2 + C n 3 + . . . + C n n = 2 n − 1 C_{n}^{1}+C_{n}^{2}+C_{n}^{3}+...+C_{n}^{n}=2^n-1 Cn1+Cn2+Cn3+...+Cnn=2n1

②每次两个球操作所产生新小球的每一项都不会相同(因为字母不同)

③一个含x个字母的球与一个含y个字母的球进行操作得到的新小球含多少项?

根据①两个球分别有 2 x − 1 2^x-1 2x1 2 y − 1 2^y-1 2y1

那么相加就有 2 x − 1 + 2 y − 1 2^x-1+2^y-1 2x1+2y1项,相乘有 ( 2 x − 1 ) ∗ ( 2 y − 1 ) (2^x-1)*(2^y-1) (2x1)(2y1)

答案是: 2 x − 1 + 2 y − 1 + ( 2 x − 1 ) ∗ ( 2 y − 1 ) = 2 ( x + y ) − 1 2^x-1+2^y-1+(2^x-1)*(2^y-1)=2^(x+y)-1 2x1+2y1+(2x1)(2y1)=2(x+y)1

正好等于①含x+y个字母的球的最大项数!并且有②,每一项都不同,那么这个球一定是一个“轮换性”的多项式!
这就说明了两个含任意数量字母的“轮换性”球进行操作,得到的新球也具有“轮换性”,那么根据归纳最终的球也具有“轮换性”,这样就证明了答案与操作目标、顺序无关。

代码就很简单了,从头操作到尾就行了。

代码

#include<bits/stdc++.h>
using namespace std;
int n,a[510];
const int mod=998244353;
void solve()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    long long ans=a[1];
    for(int i=2;i<=n;i++)
    {
        ans=(ans+a[i]+ans*a[i]%mod)%mod;
    }
    cout<<ans<<endl;
}
int main()
{
    int test;
    cin>>test; 
    while(test--)
    {
        solve();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值