专项训练3

数据结构专题

干脆直接叫线段树专题算了。

1.【模板】线段树分裂

link:https://www.luogu.com.cn/problem/P5494

线段树的所有板子套到一起,个人觉得也不是很难,也不想多浪费口舌了。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+5;
int n, m, a[maxn], bac[maxn], cnt, tot, rt[maxn], idx=1;
struct seg_tree{
	int l, r, num;
}tr[maxn<<4];
int newnode()
{
	return cnt?bac[cnt--]:++tot;
}
void del(int id)
{
	bac[++cnt]=id;
	tr[id].l=tr[id].r=tr[id].num=0;
}
void update(int &id,int l,int r,int v,int t)
{
	if(!id) id=newnode();
	tr[id].num+=t;
	if(l==r) return ;
	int mid=(l+r)>>1;
	if(v<=mid) update(tr[id].l, l, mid, v, t);
	else update(tr[id].r, mid+1, r, v, t);
}
int query(int id,int l,int r,int ll,int rr)
{
	if(r<ll||l>rr) return 0;
	if(l>=ll&&r<=rr)
	{
		return tr[id].num;
	}
	int mid=(l+r)>>1;
	if(rr<=mid) return query(tr[id].l, l, mid, ll, rr);
	else if(ll>mid) return query(tr[id].r, mid+1, r, ll, rr);
	else return query(tr[id].l, l, mid, ll, mid)+query(tr[id].r, mid+1, r, mid+1, rr);
}
int kth(int id,int l,int r,int k)
{
	if(l==r) return l;
	int mid=(l+r)>>1;
	int s1=tr[tr[id].l].num, s2=tr[tr[id].r].num;
	if(k<=s1) return kth(tr[id].l, l, mid, k);
	else return kth(tr[id].r, mid+1, r, k-s1);
}
int merge(int a, int b,int l,int r) 
{
	if(!a||!b) 
	{
		return a+b;
	}
	if(l==r) 
	{
		tr[a].num+=tr[b].num;
		del(b);
		return a;
	}
	int mid=(l+r)>>1;
	tr[a].l=merge(tr[a].l, tr[b].l, l, mid);
	tr[a].r=merge(tr[a].r, tr[b].r, mid+1, r);
	tr[a].num=tr[tr[a].l].num+tr[tr[a].r].num;
	del(b);
	return a;
}
void split(int a,int &b,int k)
{
	if(!a) return ;
	b=newnode();
	int v=tr[tr[a].l].num;
	if(k>v) split(tr[a].r, tr[b].r, k-v);
	else swap(tr[a].r, tr[b].r);
	if(k<v) split(tr[a].l, tr[b].l, k);
	tr[b].num=tr[a].num-k;
	tr[a].num=k;
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		update(rt[1], 1, n, i, a[i]);
	}
	while(m--)
	{
		int opt, p, x, y;
		cin>>opt;
		if(opt==0)
		{
			cin>>p>>x>>y;
			int k1=query(rt[p], 1, n, 1, y), k2=query(rt[p], 1, n, x, y);
			int tmp=0;
			split(rt[p], rt[++idx], k1-k2);
			split(rt[idx], tmp, k2);
			rt[p]=merge(rt[p], tmp, 1, n);
		}
		else if(opt==1)
		{
			cin>>p>>x;
			rt[p]=merge(rt[p], rt[x], 1, n);
		}
		else if(opt==2)
		{
			cin>>p>>x>>y;
			update(rt[p], 1, n, y, x);
		}
		else if(opt==3)
		{
			cin>>p>>x>>y;
			cout<<query(rt[p], 1, n, x, y)<<endl;
		}
		else 
		{
			cin>>p>>x;
			if(tr[rt[p]].num<x) cout<<-1<<endl;
			else cout<<kth(rt[p], 1, n, x)<<endl;
		}
	}
	return 0;
}

2. 三元上升子序列

link:https://www.luogu.com.cn/problem/P1637

煎蛋题,发现只需要枚举中间数,然后用权值线段树统计 前面比当前数小的个数 和 后面比当前数大的个数的乘积,最后相加即可。

Tips:RE越界不只是循环,一些+1、-1啊注意特判!!!

3. Sanae and Giant Robot

详见专项训练2-并查集专项。

4. [COCI2010-2011#6] STEP

link:https://www.luogu.com.cn/problem/P6492

小清新版山海经,合并很套路。

遇到要求一些满足特殊要求的子序列、子串长度,考虑维护最大子段和版线段树

5. [USACO11DEC] Grass Planting G

link:https://www.luogu.com.cn/problem/P3038

树链剖分板子题(话说这个提单前面怎么这么水啊)。

Tips:RE越界不只是循环,一些+1、-1啊注意特判!!!

6.「TOCO Round 1」History

link:https://www.luogu.com.cn/problem/P7394

收获很大啊。

首先遇到第三个操作:回到第 \(i\) 次事件后的状态,发现不会了,现在引入一个写的知识点:操作树!思路也是很简单的,就是离线+建树+普通dfs回溯。

然后回过头来继续想想怎么查询,显然是要求 \(x\)\(y/2\) 级祖先的 \(y/2\) 级儿子的总和- \(x\)\((y/2-1)\) 级祖先的 \((y/2-1)\) 级儿子的总和,分开来求这两个值,会发现要求的这些点的bfn值都是连续的,而且bfn越大,k级祖先的bfn也越大,也就是说它是有单调性的!那我们就可以二分出这个满足要求的儿子的bfn值的区间的左右端点,然后用树状数组单点修改和区间查询即可。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n, m, bfn[maxn], f[maxn][20], idx, dep[maxn], a[maxn], rk[maxn], ans[maxn];
#define lowbit(x) (x&(-x))
int sum[maxn];
void update(int pos,int val)
{
	while(pos<=n)
	{
		sum[pos]+=val;
		pos+=lowbit(pos);
	}
}
int query(int pos)
{
	int res=0;
	while(pos>0)
	{
		res+=sum[pos];
		pos-=lowbit(pos);
	}
	return res;
}
struct node{
	int opt, x, y, id;
}q[maxn];
struct edge{
	int next, to;
}edge[maxn<<1], e[maxn<<1];
int head[maxn], edgenum, head1[maxn], edgenum1;
void add(int from,int to)
{
	edge[++edgenum].next=head[from];
	edge[edgenum].to=to;
	head[from]=edgenum;
}
void add1(int from,int to)
{
	e[++edgenum1].next=head1[from];
	e[edgenum1].to=to;
	head1[from]=edgenum1;
}
void bfs()
{
	queue<int> q;
	q.push(1);
	while(q.size())
	{
		int u=q.front();
		q.pop();
		dep[u]=dep[f[u][0]]+1;
		bfn[u]=++idx;
		rk[idx]=u;
		for(int i=1;i<=18;i++)	f[u][i]=f[f[u][i-1]][i-1];
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(bfn[v]) continue;
			q.push(v);
			f[v][0]=u;
		}
	}
}
int kth(int x,int k)
{
	for(int i=18;i>=0;i--)
	{
		if((1<<i)<=k&&dep[f[x][i]]<=dep[x])
		{
			x=f[x][i];
			k-=(1<<i);
		}
	}
	return bfn[x];
}
int get(int x,int k)
{
	int l=1, r=n, z=0, y=0;
	while(l<r)//bfn越大,k级祖先bfn越大 
	{
		int mid=(l+r)>>1;
		if(kth(rk[mid], k)>=x) r=mid;
		else l=mid+1;
	}
	z=l;
	l=1, r=n;
	while(l<r)
	{
		int mid=(l+r+1)>>1;
		if(kth(rk[mid], k)<=x) l=mid;
		else r=mid-1; 
	}
	y=l;
	return query(y)-query(z-1);
}
void dfs(int u,int fa)
{
	int opt=q[u].opt, x=q[u].x, y=q[u].y;
	if(opt==1)
	{
		if(a[x]) update(bfn[x], -1);
		else update(bfn[x], 1);
		a[x]^=1;
	}
	else if(opt==2)
	{
		if(y%2) ans[u]=0;
		else
		{
			y/=2;
			if(y==0) ans[u]=a[x];
			else if(dep[x]-1<y) ans[u]=0;
			else ans[u]=get(kth(x, y), y)-get(kth(x, y-1), y-1);	
		}
	}
	for(int i=head1[u];i;i=e[i].next)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v, u);
	}
	if(opt==1)
	{
		if(a[x]) update(bfn[x], -1);
		else update(bfn[x], 1);
		a[x]^=1;
	}
}
signed main()
{
	cin>>n;
	for(int i=1;i<n;i++)
	{
		int u, v;
		cin>>u>>v;
		add(u, v), add(v, u);
	}
	cin>>m;
	for(int i=1;i<=m;i++)
	{
		int opt, x, y;
		cin>>opt;
		q[i].id=i;
		if(opt==1)
		{
			cin>>x;
			q[i].opt=opt, q[i].x=x;
			add1(i-1, i);
		}
		else if(opt==2)
		{
			cin>>x>>y;
			q[i].opt=opt, q[i].x=x, q[i].y=y;
			add1(i-1, i);
		}
		else 
		{
			cin>>x;
			q[i].opt=opt, q[i].x=x;
			add1(x, i);
		}
	}
	bfs(), dfs(0, 0);
	for(int i=1;i<=m;i++)
	{
		if(q[i].opt==2)
			cout<<ans[i]<<endl;
	}
	return 0;
} 

7. Points

link:https://www.luogu.com.cn/problem/CF19D

一开始到luogu上搜这个题名结果看成一个蓝题了。。。想了半个小时才发现不对劲。
话说这个笛卡尔坐标系也没体现出来啊,亏我还专门去学了好久。

自己怎么想的已经不记得了,看了题解后觉得不是特别难啊,把add的值的横坐标作为线段树叶子节点,然后维护当前横坐标的最纵坐标,套一个set。在线段树上二分找到答案,其实应该是可以自己写出来的(但写了一半感觉很怪异就弃了),其他的难点好像也没什么了,哦,还要记得离散化。

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define lid id<<1
#define rid id<<1|1
using namespace std;
const int maxn=4e5+5;
int n, b[maxn], tot;
set<int> s[maxn];
struct seg_tree{
	int l, r, maxx;
}tr[maxn<<2];
void pushup(int id)
{
	tr[id].maxx=max(tr[lid].maxx, tr[rid].maxx);
}
void build(int id,int l,int r)
{
	tr[id].l=l, tr[id].r=r;
	if(l==r)
	{
		return ;
	}
	int mid=(l+r)>>1;
	build(lid, l, mid);
	build(rid, mid+1, r);
}
void update(int id,int l,int val)
{
	if(tr[id].l==tr[id].r)
	{
		tr[id].maxx=max(tr[id].maxx, val);
		return ;
	}
	int mid=(tr[id].l+tr[id].r)>>1;
	if(l<=mid) update(lid, l, val);
	else update(rid, l, val);
	pushup(id);
}
void modify(int id,int l,int val)
{
	if(tr[id].l==tr[id].r&&tr[id].l==l)
	{
		tr[id].maxx=val;
		return ;
	}
	int mid=(tr[id].l+tr[id].r)>>1;
	if(l<=mid) modify(lid, l, val);
	else modify(rid, l, val);
	pushup(id);
}
int query(int id,int l,int v)
{
	if(tr[id].l==tr[id].r)
	{
		if(tr[id].maxx<=v) return -1;
		return tr[id].l;
	}
	int mid=(tr[id].l+tr[id].r)>>1;
	int res=-1;
	if(l<=mid&&tr[lid].maxx>v) res=query(lid, l, v);
	if(res!=-1) return res;
	if(tr[rid].maxx>v) return query(rid, l, v);
	return -1; 
}
struct node{
	int x, y;
	string opt;
}q[maxn];
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>q[i].opt>>q[i].x>>q[i].y;
		b[++tot]=q[i].x, b[++tot]=q[i].y;
	}
	sort(b+1, b+tot+1);
	int m=unique(b+1, b+tot+1)-b-1;
	for(int i=1;i<=n;i++)
	{
		q[i].x=lower_bound(b+1, b+m+1, q[i].x)-b;
		q[i].y=lower_bound(b+1, b+m+1, q[i].y)-b;
	}
	build(1, 1, m);
	for(int i=1;i<=n;i++)
	{
		int x=q[i].x, y=q[i].y;
		string opt=q[i].opt;
		if(opt[0]=='a')
		{
			update(1, x, y);
			s[x].insert(y);
		}
		else if(opt[0]=='r')
		{
			s[x].erase(y);
			if(s[x].empty())
			{
				modify(1, x, 0);
				continue;
			}
			set<int>::iterator it=s[x].end();
			it--;
			modify(1, x, *it);
		}
		else
		{
			int ans=query(1, x+1, y);
			if(ans==-1)
			{
				cout<<ans<<endl;
				continue;
			}
			cout<<b[ans]<<" "<<b[*s[ans].upper_bound(y)]<<"\n";
		}
	}
	return 0;
} 

Tips:线段树的边界值搞清楚啊。

单调栈/单调队列

1. Cashback

link:https://www.luogu.com.cn/problem/CF940E

很可惜啊,就剩最后一个性质没有往下想了(即区间长度为c是最优的),思考的还是太少,然后设 \(dp[i]\) 表示前i位答案的最大值,比较好想的 \(dp[i]=max(dp[i-1],dp[i-c]+min_{i-c+1}^{i}a[i])\),然后就是很板子的单调队列优化dp了。

贴板子
head=1, tail=0;
for(int i=1;i<=n;i++)
{
	while(head<=tail&&q[head]<=i-k) head++;
	while(head<=tail&&a[q[tail]]>a[i]) tail--;
	q[++tail]=i;
	//转移方程 		
}

2. Cutlet

link:https://www.luogu.com.cn/problem/CF939F

神奇巧妙地dp。称正在煎的那一面为背面,裸露的为正面,设 \(dp[i][j]\) 表示前 i 秒,正面被考了 j 秒的最少反转次数,转移方程为(分翻转和不翻转讨论):

  1. \(dp[i][j]=dp[i-1][j]\).
  2. \(dp[i][j]=dp[i-1][i-j]+1\).

首先能观察到只有i在区间 \([l,r]\) 时才能翻转,所以其中有很多状态时直接从1转移来的,浪费了大量时间。所以我们就只考虑区间内的i,会发现区间中最多就翻两次,其余的可以合并一下翻转操作,看作一次和两次。分类讨论。

  • 只翻一次
    • 枚举翻转后煎了k秒,那么 \(dp[i][j]=min(dp[i-1][r-j-k])+1\)
    • 单调队列维护,枚举j+k,当k>r-l时就不合法了,所以当决策点<l-j时就不合法了。
  • 翻两次
    • 枚举翻转后背面煎了k秒,那么 \(dp[i][j]=min(dp[i-1][j-k])+2\).
    • 单调队列维护,枚举j-k,当k>r-l时就不合法了,所以当决策点<j-r+l时就不合法了。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=105, N=1e5+5;
int n, k, l, r, dp[2][N*2], fl[N], head, tail, q[N*2];
signed main()
{
	cin>>n>>k;
	memset(dp[0], 0x3f, sizeof(dp[0]));
	dp[0][0]=0;
	for(int i=1;i<=k;i++)
	{
		cin>>l>>r;
		for(int j=0;j<=n;j++)
		{
			dp[i&1][j]=dp[!(i&1)][j];
		}
		head=1, tail=0;
		for(int j=r;j>=0;j--)
		{
			while(head<=tail&&q[head]<l-j) head++;
			while(head<=tail&&dp[!(i&1)][q[tail]]>dp[!(i&1)][r-j]) tail--;
			q[++tail]=r-j;
			dp[i&1][j]=min(dp[!(i&1)][q[head]]+1, dp[i&1][j]);
		}
		head=1, tail=0;
		for(int j=0;j<=r;j++)
		{
			while(head<=tail&&q[head]<j+l-r) head++;
			while(head<=tail&&dp[!(i&1)][q[tail]]>dp[!(i&1)][j]) tail--;
			q[++tail]=j;
			dp[i&1][j]=min(dp[!(i&1)][q[head]]+2, dp[i&1][j]);
		}
	}
	if(dp[k&1][n]==0x3f3f3f3f3f3f3f3f) cout<<"Hungry";
	else cout<<"Full"<<endl<<dp[k&1][n];
	return 0;
} 

3.「CZOI-R1」消除威胁

link:https://www.luogu.com.cn/problem/P10798

嗯,先把每个点赋为他的绝对值。我的想法是找到每个点i后第一个比他大的记为r[i],然后以i为左端点,i到r[i]中任意一个为右端点的就是可能构成威胁区间,但巨大的问题是难以判断左右端点是否相同而且还有更多的区间满足要求但我没统计。会发现我们想要的就只是所有威胁区间 并判断 左右端点是取反会不会使得答案更优。所以用单调栈求出cnt[i]表示以i为左端点的威胁区间的个数(代码实现只需要在加入新点是判断是否与栈顶相等)。对于当前a[i],如果为0,那么取不取反没啥区别,他的贡献为 \(C_{cnt[i]+1}^2\) ,(这里只看威胁区间的端点),否则,设这些端点中取反m个,贡献为 \(min(C_{cnt[i]+1-m}^2+C_{m}^2)\) ,当x取 \(\frac{cnt[i]+1}{2}\) 值最小。这里给下证明:

设c=cnt[i]+1,先不看除以2

\[原式= (c+1-m)(c-m)+m(m-1) \]

\[=\,c^2-cm+c-m-cm+m^2+m^2-m \]

\[=\,c^2+c-2m(c+1-m) \]

\(c^2+c\)为定值,所以要使得 \(m(c+1-m)\) 最大,和一定差小积大,当m取 \(\frac{cnt[i]+1}{2}\)原式最大。

4. Kuzya and Homework

link:https://www.luogu.com.cn/problem/CF1582G

蛮有意思的一道题。一个值序列全为整数就说明每一个除数的质因子都相消了,是不是很像括号序列一样消右括号?那么就给每一个质因子都开一个栈来记录含有这个质因子的位置(有多个相同质因子要多次记录),为什么要用栈呢?根据后进先出的性质,假如一段把除数都消完了,那么留下的都是尽量靠前的位
置,找出消的数的最前的位置(l[i]),这个位置就表示只有左端点在l[i]及其左边时,才能满足值序列全为整数。处理出每个数的 l[i] ,答案就是满足 \(l<=min_{i=l}^rf[i]\) 的区间个数,再用一个单调栈来统计答案(l[i]是满足单调性的)。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+5;
int n, a[maxn], l[maxn], ans, p[maxn], st[maxn], tot, minn[maxn];
char b[maxn];
vector<int> s[maxn];
struct node{
	int l, val;
};
stack<node> t;
void prime()
{
	st[1]=1;	
	for(int i=2;i<maxn;i++)
	{
		if(!st[i])
		{
			p[++tot]=i;
			minn[i]=i;
		}
		for(int j=1;j<=tot&&p[j]*i<maxn;j++)
		{
			st[p[j]*i]=1;
			minn[p[j]*i]=p[j];
		}
	}
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	prime();
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	for(int i=1;i<=n;i++)
	{
		cin>>b[i];
	}
	for(int i=1;i<=n;i++)
	{
		int x=a[i];
		if(b[i]=='*')
		{
			l[i]=i;
			while(x>1)
			{
				s[minn[x]].push_back(i);
				x/=minn[x];		
			}
		}
		else
		{
			l[i]=i;
			bool flag=0;
			while(x>1) 
			{
				if(s[minn[x]].size()==0)
				{
					l[i]=0;
					flag=1;
					break;
				}
				l[i]=min(l[i], s[minn[x]].back());
				s[minn[x]].pop_back();
				x/=minn[x];
			}
		}
	}
	for(int i=n;i>=1;i--)
	{
	//	cout<<l[i]<<" ";
		int res=1;
		while(!t.empty()&&t.top().l>=l[i])
		{
			res+=t.top().val;
			t.pop();
		}
		t.push((node){l[i],res});
		if(l[i]==i) ans+=res; 
	}
	cout<<ans;
	return 0;
}

5. [HNOI2016] 序列

link:https://www.luogu.com.cn/problem/P3246

想办法找到每一个i数左边第一个比他小的数的位置 \(pre[i]\) ,右边找到第一个比他小的数的位置 \(suf[i]\),然后这段区间的最小值就是当前这个数了,这个数的贡献为 \(a[i]*(i-pre[i])*(suf[i]-i)\)。但是题目是问了一堆区间,难以处理。所以考虑莫队,每次只用考虑增量。沿用上面的思路,设 \(fr[i]\) 表示以 i 结尾的区间的答案,显然对于每个i,只有 \([pre[i],i]\) 这个区间答案是 \(a[i]\) ,所以转移方程为 \(fr[i]=fr[pre[i]]+a[i]*(i-pre[i])\),同理反着处理出 \(fl[i]\)。这个数组有什么帮助呢?从一个区间 \([l,r]\) 转移到 \([l,r+1]\) 时也就是增加了 \([l,r+1],[l+1,r+1],[l+2,r+1]\) 这些区间的最小值,我们可以利用像前缀和一样的思路(但并不完全是),把一个区间 \([l,r+1]\) 分成两部分(p为这个区间最小值的位置):\([l,p]\)\([p,r+1]\) ,左端点在前者的区间贡献很显然,后者可以直接用\(fr[r+1]-fr[p]\)。可能你会问既然如此,为什么不直接用\(fr[r+1]-fr[l]\)(实际上是我会问)?因为最小值是没有可减性的\(fr[r+1]\) 不一定会从l一步一步转移过来,至于\(fr[r+1]-fr[p]\)可以这么用是因为\(fr[r+1]\) 一定会从p一步一步转移过来,这里直接相减是可以的。

数学2

1. [六省联考 2017] 期末考试

link:https://www.luogu.com.cn/problem/P3745

先把这个题放前面是因为此题的一种简单做法比T1好想。但应该也只仅限于好想了。第一篇题解思路。很一眼的想法就是找到一个统一公布时间t,比t晚的科目想办法调到t这个位置,使得总花费最少。答案没有单调性,不能二分,干脆直接枚举统一时间。现在考虑如何求当前时间的最少花费。题目中的两种操作都可以使得一些科目时间前移,每次操作第一个的代价为A+一科目后移一天,第二个代价为B。分类讨论:

  1. A>=B(=放在这个位置还是相对合适一点)
    肯定选B了,A的代价要多很多。
  2. A<B
    有能往后拖的科目就拿来和往前移的科目替换,每次代价为A,如果实在没有<t的科目了,再用B来前移。

复杂度看起来很玄学,实际上只需要前缀和就可以优化到 O(n).

讲一下代码实现:正序枚举时间,sum记录下所有时间≤t的科目的总时间,t1记录对应的个数,同样记录下>t的相应的两个值记为sumb和nb,这样就能很轻松地表示出>t的总天数和<t的总天数。学生需求以同样的方式计算。

点击查看代码
//代码太过巧妙 
#include<bits/stdc++.h>
#define ull unsigned long long 
using namespace std;
const int maxn=1e5+5;
ull A, B, C, n, m, t[maxn], b[maxn], maxx, tt[maxn], tb[maxn];
ull sumt, sumb, nt, nb, sum, t1, ans=2e18;
signed main()
{
	cin>>A>>B>>C>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>t[i];
		tt[t[i]]++;
	}
	for(int i=1;i<=m;i++)
	{
		cin>>b[i];
		tb[b[i]]++;
		sumb+=b[i];
		nb++;
		maxx=max(maxx, b[i]);
	}
	for(int i=0;i<=maxx;i++)
	{
		ull res=0;
		sum+=i*tb[i], t1+=tb[i];
		sumb-=i*tb[i], nb-=tb[i];
		sumb=max(0ull, sumb), nb=max(0ull, nb);
		sumt+=i*tt[i], nt+=tt[i];
		if(sum==0) continue;
		if(A>=B)
		{
			res+=max(0ull, sumb-nb*i)*B;
		}
		else
		{
			ull r=max(0ull, sumb-nb*i), l=max(0ull,t1*i-sum);
			res+=max(0ull, min(l, r))*A;
			r-=max(0ull, min(l, r));
			if(r) res+=r*B;
		}
		res+=max(0ull, nt*i-sumt)*C;
		ans=min(ans, res);
	}
	cout<<ans;
	return 0;
}

2. [NOI2015] 寿司晚宴

link:https://www.luogu.com.cn/problem/P2150

不像其他数论题那么恶心,但是也不好想。

3. 追寻 | Pursuit of Dream

link:https://www.luogu.com.cn/problem/P8967

趁热打铁。一晚上终于看懂了。会发现有用的点只有k个,剩下的点的期望都是一样的。设 \(dp[i]\) 表示第 \(i\) 个点到终点的期望,答案即为 \(dp[0]\) (0为起点)。每个dp值由两个部分组成,直接到达终点和散入天迹后到达终点。

  • 对于前者
    先算出n维的坐标与终点的距离,即 \(s[i]=\sum_{j=1}^{n}d[j]-a[i][j]\) ,这就是总共要走的步数。但是我们并不确定这些步数的前后关系(即不知道先走的哪一维),可以抽象成 \(s[i]\) 中有 \(d[1]-a[i][1]\) 个x,\(d[2]-a[i][2]\) 个y等等,要求 \(s[i]\) 的全排列。这是多重集全排列问题。总方案数为:\(\frac{s[i]!}{∏_{j=1}^{n}(d[j]-a[i][j])!}\),设他为t,即合法步数。那么概率为 \(q[i]=\frac{t}{n^{s[i]}}*(1-p[i])^{s[i]}\) ,期望为 \(q[i]s[i]\)
  • 对于后者
    首先想到的是从一个点散入天际后回到终点这个过程的期望是可以提前算出来的,为\(g=\sum_{i=1}^k\frac{p_i}{p}dp[i]\)。现在缺的是从i点出发,在终点前散入天际的期望,显然不好求,考虑容斥,它就等于从i点走散入天际的期望-从i点走到达终点后散入天际的期望,前者用求导可以证明为 \(\frac{1}{p}\) ,后者为 \(q_is_i+\frac{q_i}{p}\) ,最终狮子就是 \(dp[i]=q[i]s[i]+(1-q[i])g+\frac{q}{p}-q[i]s[i]-\frac{q[i]}{p}\)

然后推一下狮子把g解出来再带到dp里算一下就好了。

4. 随机漫游

link:https://www.luogu.com.cn/problem/P4321

方程是很好推的,但状态设定是想不到的。观察到n很小,直接状压。设 \(dp[s][u]\) 表示走过的点集为s,当前在节点i的期望(s包括u)。显然 \(dp[s][u]=\frac{1}{deg_u}\sum_{u→v}dp[s|v][v]+1\)。但是这个转移可能是从同层的转移而来,换言之,\(s|v=s\),此时就形成了环,考虑用高斯消元求解,也就是说列 \(n2^n\) 个方程,复杂度爆表。把这个方程变换一下,\(dp[s][u]=\frac{1}{deg_u}(\sum_{v∈s}dp[s][v]+\sum_{v!∈s}dp[s|v][v]+1)\),等号右边部分后者是知道的,把前者移过去,​\(dp[s][u]-\frac{1}{deg_u}\sum_{v∈s}dp[s][v]=\frac{1}{deg_u}\sum_{v!∈s}dp[s|v][v]+1\)。所以此时可以对每一个s单独构造一个方程组(s从大到小枚举),然后求解。对每一个s单独构造一个方程组是优化的核心。

点击查看代码
//方程推对,状态设错 
#include<bits/stdc++.h>
#define int long long
#define eps 1e-6
using namespace std;
const int maxn=1e6+5, mod=998244353;
int n, e, m, c[maxn], du[maxn], edge[maxn], deg[maxn];
int pos[maxn], id[maxn], cnt, g[maxn], a[30][30], dp[maxn][30];
inline int qpow(int a,int b)
{
	int res=1;
	while(b)
	{
		if(b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}
inline void gauss(int n) 
{
	for(int i=1;i<=n;i++)
	{
		int r=i;
		for(int k=i;k<=n;k++) 
		{
			if(a[k][i]) 
			{
				swap(a[k], a[i]);
				break;
			}
		}
	//	swap(a[r], a[i]);
		for(int k=1;k<=n;k++) 
		{
			if(!a[k][i]||k==i) continue;
			int x=a[k][i]*qpow(a[i][i],mod-2)%mod;
			for(int j=i;j<=n+1;j++) 
			{
				a[k][j]=((a[k][j]-x*a[i][j]%mod)%mod+mod)%mod;
			}
		}
	}
	for(int i=1;i<=n;i++) a[i][n+1]=a[i][n+1]*qpow(a[i][i],mod-2)%mod;
	return;
}
inline void init()
{
    g[0]=1;
    for(int i=1;i<=n;i++)
    {
        g[i]=(g[i-1]*qpow(i, mod-2))%mod;
    }
}
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin>>n>>e;
	int tot=(1<<n)-1;
	for(int i=1;i<=e;i++)
	{
		int u, v;
		cin>>u>>v;
		edge[u]|=(1<<v-1), edge[v]|=(1<<u-1);
		du[u]++, du[v]++;
	}
	for(int s=tot-1;s>=1;s--)//按集合大小倒推 
	{
		memset(a, 0, sizeof(a));
		for(int i=1;i<=n;i++)
		{
			if(s&(1<<i-1))
			{
				a[i][i]=a[i][n+1]=1;
				int x=qpow(du[i],mod-2);
				for(int j=1;j<=n;j++)
				{
					if(edge[i]&(1<<j-1))
					{
						if(s&(1<<j-1)) a[i][j]=mod-x;
						else a[i][n+1]=(a[i][n+1]+x*dp[s|(1<<j-1)][j]%mod)%mod;
					}
				}
			}
		}
		gauss(n);
		for(int i=1;i<=n;i++) 
		{
			if(s&(1<<i-1)) dp[s][i]=a[i][n+1];
		}
	}
	cin>>m;
	while(m--)
	{
		int n, u=0;
		cin>>n;
		for(int i=1;i<=n;i++) cin>>c[i], u|=(1<<(c[i]-1));
		int s=0;
		cin>>s;
		cout<<dp[(tot^u)|(1<<(s-1))][s]<<endl;
	}
	return 0;
}

5. [JLOI2014] 路径规划

link:https://www.luogu.com.cn/problem/P3259

平均时间是什么?平均数?期望?答案是期望,这里直接给出:一个红绿灯的期望为 \(\frac{a^2}{2(a+b)}\) ,由于证明需要微积分等知识(虽然一次函数也可以),就不写了。

这道题就是在最短路上加了两个限制:红绿灯和加油站,考虑使用分层图,由于k相比于加油站要小,考虑先建k层(称这个分层图为G1),跑最短路,这样子就只剩加油站一个限制了。可以把加油这一限制也看成边,把起点、终点和加油站全部单拎出来(所有层的),称这些点为G2,会发现其他点已经没什么用了,我们要的也就只是个边权,将G2里面两点最短路小于limit的重新连边,边权为G1上的最短路+cost,形成了一个新的分层图。再在这个新图上跑最短路即可。复杂度可以证明是能过的。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e6+5;
int n, m, k, lim, cost, st, ed;
int head[maxn], edgenum, vis[maxn], edgenum1, head1[maxn];
double dis[maxn];
map<string,pair<int,double> > id;
struct edge{
    int next;
    int to;
    double w;
}edge[maxn<<1], edge1[maxn<<1];
void add(int from,int to,double w)
{
    edge[++edgenum].next=head[from];
    edge[edgenum].to=to;
    edge[edgenum].w=w;
    head[from]=edgenum;
}
void add1(int from,int to,double w)
{
    edge1[++edgenum1].next=head1[from];
    edge1[edgenum1].to=to;
    edge1[edgenum1].w=w;
    head1[from]=edgenum1;
}
struct node{
	double dis;
	int pos;
	bool operator<(const node &x) const
	{
		return x.dis<dis;
	}
};
inline void dijkstra(int s)
{
	for(int i=1;i<=n*(k+1);i++) dis[i]=0x3f3f3f3f3f3f3f3f, vis[i]=0;
	priority_queue<node> q;
	dis[s]=0, q.push((node){0, s});
	while(q.size())
	{
		int u=q.top().pos;
		q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(int i=head[u];i;i=edge[i].next)
		{
			int v=edge[i].to;
			if(dis[v]>dis[u]+edge[i].w)
			{
				dis[v]=edge[i].w+dis[u];
				q.push((node){dis[v], v});
			}
		}
	}
}
inline void dij(int s)
{
	for(int i=1;i<=n*(k+1);i++) dis[i]=0x3f3f3f3f3f3f3f3f, vis[i]=0;
	priority_queue<node> q;
	dis[s]=0, q.push((node){0, s});
	while(q.size())
	{
		int u=q.top().pos;
		q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(int i=head1[u];i;i=edge1[i].next)
		{
			int v=edge1[i].to;
			if(dis[v]>dis[u]+edge1[i].w)
			{
				dis[v]=edge1[i].w+dis[u];
				q.push((node){dis[v], v});
			}
		}
	}
}
vector<int> gas;
signed main()
{
	cin>>n>>m>>k>>lim>>cost;
	for(int i=1;i<=n;i++)
	{
		string s;
		cin>>s;
		if(s.find("gas")!=string::npos)
		{
			gas.push_back(i);
		}
		if(s=="start") st=i, gas.push_back(i);
		else if(s=="end") ed=i, gas.push_back(i);
		int a, b;
		cin>>a>>b;
		id[s]=make_pair(i, a*a*1.0/(2*(a+b)));
	}
	for(int i=1;i<=m;i++)
	{
		string s1, s2, s3;
		int w;
		cin>>s1>>s2>>s3;
		for(int j=0;j<=k;j++)//建分层图 
		{
			if(id[s2].second)
				add(j*n+id[s1].first, (j+1)*n+id[s2].first, w+id[s2].second);
			else
				add(j*n+id[s1].first, j*n+id[s2].first, w);
			if(id[s1].second)
				add(j*n+id[s2].first, (j+1)*n+id[s1].first, w+id[s1].second);
			else
				add(j*n+id[s2].first, j*n+id[s1].first, w);
		}
	}
	for(int i:gas)
	{
		dijkstra(i);
		for(int j:gas)
		{
			if(i==j) continue;
			int w=(i==st||i==ed?0:cost);
			for(int l=0;l<=k;l++)
			{
				if(dis[l*n+j]<=lim)
				{
					for(int p=0;p+l<=k;p++)
					{
						add1(p*n+i, (p+l)*n+j, dis[l*n+j]+w);
						add1((p+l)*n+j, p*n+i, dis[l*n+j]+w);	
					}
				}		
			}
		}
	}
	dij(st);
//	cout<<"qwq";
	double ans=1e9;
	for(int i=0;i<=k;i++)
	{
		ans=min(ans, dis[i*n+ed]);
	}
	printf("%.3lf",ans);
	return 0;
} 

“当你真心渴望某样东西时,整个宇宙都会联合起来帮助你完成。”

posted @ 2025-02-28 18:09  zhouyiran2011  阅读(31)  评论(0)    收藏  举报