做题记录

CF2008 G. Sakurako's Task

纪念第一次场上切的一道 G 题,想了 40 多分钟。

题意

给定数组 a,可以进行任意次操作:选定 i,j, 可以操作 aiai+ajaiaiaj
问 数组 a 中不存在的第 k 个非负整数的最大可能为多少。

思路

首先发现这个第 k 个非负整数比较奇怪,不太好入手。但是可以发现尽量把数往小了堆是更优的。
转变思路从操作入手,发现如果我们可以获得一个很小的数 d,且其他数都是 d 的倍数,那么我们可以先把所有数都变成 0,只剩下一个 d,然后可以将数组构造成 0,d,2d,3d(n1)d,保证数都是最小的。

那么思考如何得到最小的 d,对于两个数 a,b ,我们可以用类似辗转相减的方法得到 gcd(a,b),同理对于 ai,我们可以得到最小的 d=gcd(a1,a2,an),于是本题就做完了。

Artoj P3183. 游戏升级

模拟赛的好题。

题目大意

t 组询问,每次给定 a1,a2,c,n,求:

x=1n[a1xa2x=c]

整除分块很一眼,然后想法是先对 a1 做一遍用哈希记录,然后做 a2 时添加答案。

但是 哈希会超时。
然后可以想到先 对a1,a2 做完,然后对于一块答案相同的部分存在数组里面,然后就可以用双指针来做,少了一个 log。复杂度 O(nn)

#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 的方式都是 f2k1x+f2ky=X,其中 fi 为斐波那契的第 i 项。然后发现这个式子形如 ax+by=c , 可以用 exgcd 来解。

做到这里以为没了,但实际上有个地方没有考虑到:算法在枚举斐波那契数列时,对于不同的 a,b,算出来的 x,y 是否可能重复。

我们假设存在:

{a1x+b1y=ca2x+b2y=c

我们发现解出来 y 为负数,不符合条件,因此 x,y 并不会重复。
但是比较特别的是当 c=0 时,x=0,y=0,会被重复计算,因此需要特判 c = 0。
本题就做完了。

三元组

模拟赛遇到的好题,考验对 trie 树的理解。

一句话题意:给定一个序列 an<=5×105,ai<=230,问 (i,j,k) 满足 (ai xor aj)<(aj xor ak) 的三元组数量。

std 是枚举 i,但好像枚举 j 也可以做。

做法

枚举 i,按位考虑发现可以从 i,k 从第几位开始不同考虑。
因为前几位相同时结果一定是相同的。
于是我们可以在trie树上枚举 ak 从第 t 位开始与 ai 不同。
而 j 可选的条件是 aj 的第 t 位等于 ai 的第 t 位。

因此对于每个 k 贡献就为 prek,t,cprei,t,c
可以将贡献拆开,每个节点维护 prek,t,c 的和还有 k 的数量。
cai 的第 t 位。
此时对于每个 k 都要预处理出每一位,复杂度 nlog2V,过不去。
考虑优化,我们发现对于每个 k ,需要查询的每次都是其所在的那一位,因此只需要保存所在那一位即可。

具体实现为对于 trie 树上每个节点保存一个 sum,表示所有 k 的 prek,c 的和。
还有保存一个 cnt,表示 k 的数量。
然后枚举 i,贡献为 sumcntpre[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] 概率充电器

P4284 [SHOI2014] 概率充电器

感觉是一道非常好的题啊。

树形 DP + 概率期望。

题意:给定一棵树,每个点有概率自己带电,每条边也有概率可以传导电,问期望带电的点的个数。

做法

首先是一个非常简单经典的 trick,看到期望但是值只有 1,于是可以将期望转化成求每个点被点亮的概率之和。

于是考虑在树上做树形 DP。

首先我们可以设出 gx 表示考虑 x 及其子树,使得 x 带电的概率。

这个转移非常好想:gx=1(1gson×px,son)

从反面考虑一下就做完了。

然后需要考虑父亲对当前 x 的影响,设 f[x] 表示 x 带电的概率。

我们发现父亲能对 x 产生影响当且仅当父亲不是由 x 点亮的。

因此设 p1 表示父亲不是由 x 点亮的概率,则有:

p1=(1ffa)1pfa,xgx

那么转移就为:

fx=1(1gx)×(1p1px,fa)

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

Codeforces Round 972 (Div. 2)

VP打的,结果连 C 都做不出来 /qd /qd

题意:

给定 n 个字符串,任选几个按顺序拼在一起,设为字符串 S,令 cntS"narek" 的数量,问 max{S.sizecnt5}

一开始想的 DPdpi 表示考虑前 i 个的答案。然后转移考虑每个串末尾的可能。但是发现一个问题就是只能考虑到当前这个和前一个的贡献。换句话说 narek 跨过几个字符串的情况无法考虑,遂破防看题解。

正解

非常巧妙的 DP 啊。设 dp[i] 表示当前已经匹配了 i 个字符,正在匹配 "narek" 中的第 i1 个的答案。

为什么能这样设置呢?因为我们可以发现答案与当前是第几个字符串没有什么关系,而且从一个一个字符匹配 "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×2 才是答案。

CSP-S 模拟赛 A. 序列

给点序列 a1,a2,,an,问有多少非负整数序列 x1,x2,,xn 满足:

  • i,0xiai
  • 满足 x1|x2||xn=x1+x2++xn

n16,ai<260

状压+数位 DP 好题。

条件二可以转化为 x 两两并为 0。按位考虑,即每一位至多一个 1

考虑从高位开始给每一位填数,那么需要判断是否超过 ai ,可以想到用数位 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 轮游戏,每轮有 ai,bi,ci,每轮开始 A += D。

接下来选择一项:

  • S +=A+ai
  • D +=bi
  • A +=ci

S 的最终最大值。

考虑计算每一项操作对于 S 的贡献,考虑需要计算记录哪些东西。

操作一可以直接加。

对于操作三,若我们知道后面攻击了 cnt 次,则我们可以知道该操作对于 S 的贡献为 cnt×ci

但是操作二似乎没有那么好记录,发现增量对于 S 的贡献大概是 j(ji)bi,其中 ji 后面选择操作 1 的游戏轮。

那么我们发现只需要知道 i 后面所有的 j 的和。

则状态设计为:dpi,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(n4),有点小卡。

A. 中位数

来源:2024 CSP-S 模拟赛 Day 15

你有一个序列 a1,a2,,an

定义 bl,r 表示序列 {ai}lir 的中位数。定义 cl,r 为序列 {bi,j}lijr 的中位数。

{ci,j}lijr 的中位数是多少。

对于一个大小为 m 的序列,我们定义它的中位数为第 m2 小的数字。

一个经典的trick:aix1,ai<x0

看到中位数,可以想到先二分中位数为 x

然后我们将大于等于 x 的数看成 1,小于 x 的数看成 0,得到一个新序列。

则通过我们在新序列得到的答案就可以知道答案与 x 的大小关系。

在新序列上考虑原问题。

首先 bl,r 可以通过一维前缀和直接判断,当 sumrsuml1>(rl+1)2bl,r1

同理,cl,r 也可以用二维前缀和算出来。

然后这题就做完了。

和这题同个 trick 的经典题目:P2824 [HEOI2016/TJOI2016] 排序

题意是区间排序操作,在操作最后单点查询。

做法是二分答案,然后 aix1,ai<x0
那么区间排序就可以用区间赋值 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

[ABC282Ex] Min + Sum

一道最小值分治的 trick。

又称笛卡尔树分治。

题意:求 (l,r) 对数满足 1lrni=lrBi+mini=lrAiS

做法考虑分治,将 mid 设为区间最小值的位置,这一步可以用 st 表做。

然后将将原区间 [l,r] 划分成 [l,mid1],[mid+1,r] ,因此我们只需要考虑 (l,r) 跨过 mid 的贡献。

我们发现由于区间和的单调性,在确定 l,r 其中一个时,另一个可以二分求得。

同时为了保证复杂度,我们枚举左右中长度较小的区间,于是本题就做完了。

复杂度分析

题解区还看到一种更nb的做法,直接用单调栈算出该值作为最小值的最左和最右,然后直接枚举左右区间中更小的区间,二分计算。

这两种方法的复杂度都可以用从笛卡尔树的角度证明。

考虑每个位置 i 在暴力枚举中产生了多少贡献。

设当前区间节点为 x,而位置 ix 的子树 y 中,且 yx 较小的子树。

那么在枚举完子树 y 后,下一次再枚举到 i 时,就应是 x 作为子树进行枚举。

而又有 sizx2×sizy ,于是我们发现每一次枚举 i 的时候,其所在区间就翻倍。

也就是说每个位置最多被枚举 O(nlogn) 次。

类似启发式合并。

#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 的序列 ai,满足 1n18,0ai107。定义一次操作为将 [l,r] 区间赋值为 al,,armex 值,求在 5×105 次操作之内序列和的最大值,并给出操作序列。

分析

题目要求求出最大值并给出构造方案。

我们先思考题目给出的操作有什么性质。

由于题目给出的限制 cnt<=5×105 非常大,所以我们大胆尝试。

通过手玩样例,我们猜测对于任意区间 [l,r] 我们都可以将其中每个数变成 rl+1 ,即区间长度。

等下再证明这玩意,先看看知道了这条性质怎么求出最大值。

考虑 DP,设 fi 表示前 i 个能得到的最大值,直接枚举 j 转移:

fi=max{f[j1]+(ij+1)(ij+1)}

过程中记录一下上次的转移点,就可以知道操作了哪些区间。

再考虑刚才的操作,我们假设区间长度 len=5

而最后的状态是

5 5 5 5 5

想要达成这个状态,必不可少的是

4 3 2 1 0

func(l,r) 表示对 [l,r] 进行一次操作。

gi 表示在原区间形成 i,i1,i2,0 的所需操作集。

我们发现 g4=g3+func(1,5)+g3

于是我们可以得出 gi=gi1+func(l,r)+gi1

而操作数刚好为 fi=2i1

218+185×105 稳稳通过!

2028E - Alice's Adventures in the Rabbit Hole

题意

给定一棵树,皇后想处死爱丽丝,而爱丽丝想逃出去。爱丽丝若在叶子节点则被处死,在根节点则逃出。

每次操作都有 12 的概率由皇后或爱丽丝来操作一次操作可以将爱丽丝移动到相邻的节点。

问爱丽丝起点在 1,2,,n 时,爱丽丝逃出的概率。

做法

先考虑一条链的情况:

1,2,3,,d,,n

假设起点在 i 的答案为 fi

则有

fi=12×fi1+12×fi+1

f1=1,fn=0

2fi=fi1+fi+1

fifi1=fi+1fi=d

f 是一个等差数列,所以:

fn=f1+(n1)×d

d=1n1

通项为:

fi=1i1n1

我们在树上做一个短链剖分

解释一下,就是把树链剖分的重儿子变成到最近叶子的距离最小的儿子。

对于每条短链,我们需要额外算上链顶的父亲。

然后就可以将其当做链上的情况了。

假设当前在 i,pi 跑到链的父亲的概率,则 1pi 跑到叶子的概率。

因为当爱丽丝跑到当前链的父亲上时,此时已经换了一条链了,所以需要分开考虑。

则有:

fi=p×ffa[top]+(1p)×0

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 个人站成一行,能力值分别为 a1,a2,,an

会进行 n1 轮比赛。每次比赛,裁判会选出两个序列中相邻的选手,其中能力值更高的选手将获得胜利,而另一位选手会被淘汰。当两个人能力相同的时候,你可以任意选择胜者。败者会离开队伍,而胜者的能力值会增加 1,并且剩下的人合并成一行,继续进行比赛。

你可以任意选择比赛顺序和相同情况下的胜者,问最后哪些选手可能成为最后的赢家。从小到大输出这些选手的编号。

做法

我们对于原序列建笛卡尔树,首先最大值是一定可以赢的。

然后我们观察到每个节点可以将其子树所有点吃掉。

设选手 i 能否赢为 ansi

于是我们可以列出 dp:fi 表示我们将子树 i 删掉,取代为一个权值为 fi 的数且 ansi=1,满足 fi 最小。

可以得到:

  • fiafa

  • fiffa(sizfasizi)

于是 fi 为两者取个 max

从上往下做就做完了。

posted @   codwarm  阅读(59)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示