Logo lzx 的博客

博客

标签
暂无

CF1637F Towers 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-08-29 22:05:29

CF1637F Towers 题解

题意

给你一棵树,树上的每一个节点都有一个高度,编号为 $i$ 的点的高度为 $h_i$。你可以在这些点中选任意多的点建信号塔,建一座权值为 $e$ 的塔花费为 $e$。

我们称一个点收到了信号,当且仅当存在一条路径,使得路径上的两个端点的权值都大于等于这个点的高度,问你令每个点都收到信号的最小花费。

思路

(温馨提示:下文中的高度并不是节点深度)

首先不难想到,每一座塔一定建在树的叶子上,因为任何非叶节点都能被叶节点替代。如果我在某个非叶节点上建了塔,为了满足要求,一定有另一个节点的权值大于等于这个节点的高度,那么我完全可以找到子树内的另一个叶节点,让它的权值大于等于当前节点的高度,这样的花费一定是更小的;另一方面,为了满足每个点都被覆盖的要求,每个叶节点一定有塔。所以,叶结点上一定有塔,而且只有叶结点上有塔。

上面的论述启发我们想到贪心的思路。首先,对于最大值,一定有两个叶节点上的塔的权值是最大值;对于其他的节点,我们令路径的一个端点是其中的一个最大值,另一个端点是当前节点子树内的某一个叶子,这样即可满足要求。

那么我们可以这么做:以一个高度最大的点为根,对于非根节点,如果当前节点的高度大于子树内的权值最大的塔的权值,那么就让这个权值变成当前节点的高度,否则不变;对于根节点,我们令整棵树权值最大的两个叶子的权值都变成最大的高度。不难证明,这样一定是最优的。

不理解可结合代码食用。

时间复杂度 $O(n)$。

代码

#include<bits\/stdc++.h>
#define int long long
#define endl '\n'
#define fi first
#define se second
using namespace std;
const int N=2e5+10;
const int inf=0x3f3f3f3f3f3f3f3f;
int n;
int h[N];
int ans;
vector<int> G[N];
int dfs(int u,int fa)
{
	int maxn1=0,maxn2=0;
	for(auto v:G[u])
	{
		if(v==fa) continue;
		int p=dfs(v,u);
		if(p>maxn1) maxn2=maxn1,maxn1=p;
		else if(p>maxn2) maxn2=p;
	}
	if(fa!=0) ans+=max(0ll,h[u]-maxn1);
	else ans+=max(0ll,h[u]-maxn1)+max(0ll,h[u]-maxn2);
	maxn1=max(maxn1,h[u]);
	return maxn1;
}
signed main()
{
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false); 
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>h[i];
	}
	for(int i=1;i<n;i++)
	{
		int u,v;
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u); 
	}
	int s=1;
	for(int i=1;i<=n;i++)
	{
		if(h[i]>h[s]) s=i;
	}
	dfs(s,0);
	cout<<ans;
	return 0;
}

CF1994F Stardew Valley 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-09-13 16:27:14

题目传送门:

luogu

CF1994F

题意

给你一张 $n$ 个点 $m$ 条边的图,上面有重要的和不重要的边,重要的边必须走,不重要的可走可不走,问你能不能构造出欧拉回路。

思路

首先,我们可以先把所有重要的边放到实际的图里,然后考虑加上某一些不重要的边,使得这张图上有欧拉回路。

让我们考虑如何构造。

首先,题目里已经保证了重要的边可以使得图联通,那么我们只用考虑度数的问题。我们只把不重要的边拿出来,我们发现,在一个连通块内,如果有奇数个奇度点,那么无论如何加边,我们都无法使得这张图满足条件,因为加边后奇度点的个数一定是 $+2$ 或者 $-2$ 的关系。

否则,贪心的加边,用不重要的边去跑出一个生成树森林,如果一个点是奇度点,那么就在实际的图上加上这个点到它父亲的边。到最后,如果根节点的度数是奇数,此时所有的非根节点都已经是偶度点了,说明原来这个连通块就有奇数个奇度点,说明没有合法方案。

最后,在实际图上跑出欧拉回路即可。

对于一组数据,时间复杂度是 $O(n+m)$,可以通过。

温馨提示,用 vector 存图跑欧拉回路时千万不能用 auto,否则每次遍历点的时候都会从 vector 的开头开始跑,会 TLE。

Code

#include <bits\/stdc++.h>
#define endl '\n'
#define int long long
#define fi first
#define se second
using namespace std;
const int N=5e5+10;
const int inf=0x3f3f3f3f3f3f3f3f;
struct Node
{
    int v,id;
};
stack<int> st;
int t;
int n,m;
vector<Node> G[N],el[N];\/\/1边,0边 
int deg[N];
bool vis[N];\/\/点 
bool del[N];\/\/边 
int cnt;
void dfs(int u)
{
    vis[u]=1;
    for(auto x:el[u])
    {
        int v=x.v;
        int id=x.id;
        if(vis[v]) continue;
        dfs(v);
        if(deg[v])
        {
            G[u].push_back({v,id});
            G[v].push_back({u,id});
            deg[u]^=1;
            deg[v]^=1;
            cnt++;
        }
    }
}
int ne[N];
void euler(int u)
{
    for(int i=ne[u];i<G[u].size();i=ne[u])
    {
        ne[u]=i+1;
        int v=G[u][i].v;
        int id=G[u][i].id;
        if(del[id]) continue;
        del[id]=1;
        euler(v);
    }
    st.push(u);
}
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>t;
	while(t--)
	{
	    cin>>n>>m;
	    for(int i=1;i<=n;i++)
	    {
	        ne[i]=deg[i]=vis[i]=0;
	        G[i].clear();
	        el[i].clear();
        }
        for(int i=1;i<=m;i++) del[i]=0;
	    cnt=0;
	    for(int i=1;i<=m;i++)
	    {
	        int u,v,c;
	        cin>>u>>v>>c;
	        if(c)
	        {
	            G[u].push_back({v,i});
	            G[v].push_back({u,i});
	            deg[u]^=1;
	            deg[v]^=1;
	            cnt++;
            }
            else
            {
                el[u].push_back({v,i});
                el[v].push_back({u,i});
            }
        }
        bool fl=1;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i])
            {
                dfs(i);
                if(deg[i])
                {
                    fl=0;
                    break;
                }
            }
        }
        if(!fl)
        {
            cout<<"NO"<<endl;
            continue;
        }
        cout<<"YES"<<endl<<cnt<<endl;
        euler(1);
        while(!st.empty())
        {
            cout<<st.top()<<" ";
            st.pop();
        }
        cout<<endl;
    }
    return 0;
}

MX-S4-T2 youyou 不喜欢夏天 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-10-21 15:40:30

MX-S4-T2 youyou 不喜欢夏天 题解

题意

给你一个 $2 \times n$ 的网格,每个格子是黑色的或是白色的,先手选出一个四联通的连通块,后手可以将 $m$ 个列上下调换,先手要使得黑色块的数量减去白色块的数量尽量大,后手要使它尽量小,问你最终在选出的连通块里黑色块的数量减白色块的数量最大是多少。

$T$ 组数据。

$m \le n$,$\sum n \le 2\times 10^6$,$1 \le T \le 5$。

思路

首先,对于上下是全黑,一黑一白,全白,有不同的策略。

  • 对于全黑,肯定是都选更优,这样无论怎么换,都不会减少贡献。
  • 对于全白,我们肯定最多选一个,因为选两个一定不优,选一个就能维护连通性,或者直接不选。
  • 对于一黑一白,我们又有两种选法。我们可以都选上,这样的贡献是 $0$,无论他怎么换都不会影响贡献;我们也可以只选那一个黑的,但是可能被调换,造成 $-2$ 的贡献,由于他最多换 $m$ 个,所以要减去 $2 \times m$ 的贡献。

需要注意的是,对于一黑一白只选一个黑的的位置的情况,如果选的不足 $2 \times m$ 个,设选了 $k$ 个,那么在造成 $-2 \times k$ 的贡献后,一定不如把这些位置的黑白块都选上更优,所以即使算出来的贡献不是这种选法真正的贡献,但是不会影响答案。

对于每种情况,我们可以通过 dp 解决。

设 $f_{i,j},j \in \{0,1,2,3\}$ 表示到了第 $i$ 个位置,最后一个不选/选了上面那个/选了下面那个/都选了的最大贡献(其实就是状压)。那么转移的时候,一个状态能转移到另一个状态,当且仅当两个状态选的列有交集,或者直接只选这一列。

设 $add_{i,j}$ 表示第 $i$ 列不选/选了上面那个/选了下面那个/都选了的贡献。

$f_{i,j}$ 的初值即为 $add_{i,j}$。

令 $\operatorname{and}$ 表示按位与。

转移方程即为:

$$f_{i,j}=\max_{k \in \{0,1,2,3\},j \operatorname{and} k \neq 0} f_{i-1,k}+add_k,f_{i,j}$$

复杂度 $O(n)$。

不理解结合代码。

Code

#include <bits\/stdc++.h>
#define endl '\n'
#define int long long
#define fi first
#define se second
using namespace std;
const int N=2e6+10;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m;
string s[2];
int f[N][4];
int c;
int t;
int ans=-inf; 
\/*
对于上下全黑,肯定都选
对于全白,肯定最多选一个
对于一黑一白,有两种情况
1.一部分只选黑的,但有可能被调换
2.都选成上下都选,贡献为0
对于连通性,可以用状压维护 
*\/
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>c>>t;
	while(t--)
	{
	    ans=-inf;
	    cin>>n>>m;
        cin>>s[0]>>s[1];
        for(int i=0;i<=3;i++) f[0][i]=-inf;
	    for(int i=1;i<=n;i++)  \/\/第一种选法,所有的黑白都选,有0的贡献 
	    {
	        int add[4]; \/\/分别4种选法的贡献
	        add[0]=0;
	        if(s[0][i-1]=='1') add[1]=1;
	        else add[1]=-1;
	        if(s[1][i-1]=='1') add[2]=1;
	        else add[2]=-1;
	        add[3]=add[1]+add[2];
	        if(add[1]!=add[2]||(add[1]==add[2]&&add[1]==1)) add[1]=add[2]=-inf;
	        for(int j=0;j<4;j++) f[i][j]=add[j];
	        for(int j=0;j<4;j++)
	        {
	            for(int k=0;k<4;k++)
	            {
	                if(j&k) f[i][k]=max(f[i][k],f[i-1][j]+add[k]);
                }
            }
            for(int j=0;j<4;j++) ans=max(f[i][j],ans);
        }
        for(int i=1;i<=n;i++) \/\/第二种,有部分的黑白选的是黑的,对于这种选法,最多减去2*m的贡献
        \/\/需要注意的是,如果选的不足m个,那么一定没有上面的选法优,所以虽然说选出的答案不是正确的贡献,但一定不如上面的优 
        {
            int add[4]; \/\/分别4种选法的贡献
	        add[0]=0;
	        if(s[0][i-1]=='1') add[1]=1;
	        else add[1]=-1;
	        if(s[1][i-1]=='1') add[2]=1;
	        else add[2]=-1;
	        add[3]=add[1]+add[2];
	        for(int j=0;j<4;j++) f[i][j]=add[j];
	        if(add[1]==add[2]&&add[1]==1) add[1]=add[2]=-inf;
	        for(int j=0;j<4;j++)
	        {
	            for(int k=0;k<4;k++)
	            {
	                if(j&k) f[i][k]=max(f[i][k],f[i-1][j]+add[k]);
                }
            }
            for(int j=0;j<4;j++) ans=max(f[i][j]-2*m,ans);
        }
        cout<<ans<<endl;
    }
    return 0;
}

ABC379F Buildings 2 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-11-10 17:24:24

纪念第一道在场上想出正解但没写出的 F 题。

题意

有 $n$ 栋大楼排成一排,给你 $q$ 次询问,每次给出 $l$ 和 $r$,问你在 $r$ 之后有多少栋大楼可以被 $l$ 到 $r$ 之间的大楼看到,定义两个楼能看到当且仅当它们之间没有比右边的楼更高的楼。

$n \le 2 \times 10^5$,$q \le 2 \times 10^5$。

思路

首先想到 $l$ 到 $r$ 之间的楼能向后看到的楼一定是一个单调不减的序列,而且一定是最靠近 $r$ 的单调不减的序列,否则就会互相遮挡看不见。

然后我们发现,能向后看到的楼的高度一定大于等于区间内的最大值,否则最大值左边的楼就看不见这栋楼了。如果最大值在最左边,那就要大于 $l+1$ 到 $r$ 之间的最大值。

所以我们先用单调栈倒序维护以每个点为开头的最近的单调不减序列的长度,记为 $ans_i$,用线段树找到区间内的最大值,然后可以用线段树二分找到 $r$ 之后的第一个大于最大值的楼的位置,记为 $pos$,$ans_{pos}$ 即为答案。

如果还没理解可以画图,或结合下面代码食用。

Code

#include <bits\/stdc++.h>
#define endl '\n'
#define int long long
#define fi first
#define se second
using namespace std;
const int N=2e5+10;
const int inf=0x3f3f3f3f3f3f3f3f;
struct Node
{
    int l,r,val;
}tr[N<<2];
int n,q;
int a[N];
int ans[N];
inline void pushup(int rt)
{
    tr[rt].val=max(tr[rt<<1].val,tr[rt<<1|1].val);
}
void build(int rt,int l,int r)
{
    tr[rt].l=l,tr[rt].r=r;
    if(l==r)
    {
        tr[rt].val=a[l];
        return ;
    }
    int mid=(l+r)>>1;
    build(rt<<1,l,mid);
    build(rt<<1|1,mid+1,r);
    pushup(rt);
}
int qry1(int rt,int l,int r)
{
    if(l<=tr[rt].l&&tr[rt].r<=r)
    {
        return tr[rt].val;
    }
    int mid=(tr[rt].l+tr[rt].r)>>1;
    int res=-inf;
    if(l<=mid) res=max(res,qry1(rt<<1,l,r));
    if(r>mid) res=max(res,qry1(rt<<1|1,l,r));
    return res;
}
pair<int,int> qry2(int rt,int l,int r,int val)
{
    if(tr[rt].l==tr[rt].r)
    {
        return {tr[rt].val,tr[rt].l};
    }
    int mid=(tr[rt].l+tr[rt].r)>>1;
    if(l<=tr[rt].l&&tr[rt].r<=r)
    {
        if(tr[rt<<1].val>=val) return qry2(rt<<1,l,r,val);
        else return qry2(rt<<1|1,l,r,val);
    }
    if(l<=mid)
    {
        pair<int,int> p=qry2(rt<<1,l,r,val);
        if(p.fi>=val) return p;
    }
    if(r>mid) return qry2(rt<<1|1,l,r,val);
}
stack<int> st;
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>n>>q;
    for(int i=1;i<=n;i++) cin>>a[i];
	build(1,1,n);
	for(int i=n;i>=1;i--)
	{
	    while(!st.empty()&&st.top()<a[i]) st.pop();
	    st.push(a[i]);
	    ans[i]=st.size();
    }
	while(q--)
    {
        int l,r;
        cin>>l>>r;
        if(r==n)
        {
            cout<<0<<endl;
            continue;
        }
        int maxn=qry1(1,l,r);
        if(a[l]==maxn) maxn=qry1(1,l+1,r);
        if(qry1(1,r+1,n)<maxn)
        {
            cout<<0<<endl;
            continue;
        }
        cout<<ans[qry2(1,r+1,n,maxn).se]<<endl;
    }
    return 0;
}

【MX-S6-T2】「KDOI-11」飞船 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-11-17 22:22:01

题传

题意

有一艘飞船,在一条射线上飞行,初始速度为 $0$,共有 $n$ 个加油站,分别在 $p_i$ 的位置上,每加一次油花费 $t_i$ 的时间,飞船的速度乘上油的类型 $x_i$,可以选择加或者不加,飞行不消耗油,$q$ 次询问,每次给出一个位置 $y_i$,让你回答从起点到 $y_i$ 的最短时间。

$1 \le n \le 10^5$,$1 \le q \le 10^5$,$p_i \le 10^9$,$t_i \le 10^9$,$1 \le x_i \le 4$,$1 \le y_i \le 10^9$。

思路

首先注意到精度要求 $10^{-6}$,而询问最多到 $10^9$,因此,速度大于 $10^{15}$ 是没有意义的,输出 $0$ 即可。

然后我们发现,$x_i$ 的值域很小,只有 $4$,只包含 $2$ 和 $3$ 两个质因子,在 $10^{15}$ 下,分别只有 $50$ 和 $32$ 次方,于是我们可以用质因子记录下来到每个加油站达到某个的速度的最短时间,设 $f_{i,j,k}$ 表示到了 $i$ 这个点,速度为 $2^j \times 3^k$ 的最短时间,设 $s2$ 表示当前点的 $x_i$ 质因子 $2$ 的个数,$s3$ 表示质因子 $3$ 的个数,那么达到这个速度要么是在之间的加油站已经达到了,要么是新加的油,转移方程即为 $f_{i,j,k}=\min{f_{i-1,j,k}+(p_i-p_{i-1})/(2^j \times 3^k),f_{i,j-s2,k-s3}+(p_i-p_{i-1})/(2^{j-s2} \times 3^{k-s3})+t_i}$。 需要注意边界和初始值。注意要预处理出来乘方。

把 $f$ 预处理完之后,现在我们来处理询问,对于每次询问,我们可以找到它前一个加油站(位置严格小于它),枚举到那一个加油站的速度,计算出最短时间。

但是还没完,如果你精心计算,你会发现 $f$ 数组的空间巨大,我们可以将询问离线下来,排序,这样就可以压掉第一维的位置,空间完全可以接受。

令 $2$ 的最大次幂为 $V$,$3$ 的最大次幂为 $B$,时间复杂度即为 $O(VBn+m)$,完全可以接受。

可结合代码食用。

Code

#include<bits\/stdc++.h>
#define int long long
#define endl '\n'
#define fi first
#define se second
using namespace std;
const int N=1e5+10;
const int inf=0x3f3f3f3f3f3f3f3f;
const double eps=1e-6;
struct Node
{
	int p,t,x;
}sta[N];
int n,q;
pair<int,int> qry[N];
double f[50][32];
double ans[N];
double mi2[50],mi3[32];
signed main()
{
\/\/	freopen("ship.in","r",stdin);
\/\/	freopen("ship.out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>sta[i].p>>sta[i].t>>sta[i].x;
	mi2[0]=mi3[0]=1;
	for(int i=1;i<=49;i++) mi2[i]=mi2[i-1]*2;
	for(int i=1;i<=31;i++) mi3[i]=mi3[i-1]*3;
	for(int i=1;i<=q;i++)
	{
		cin>>qry[i].fi;
		qry[i].se=i;
	}
	sort(qry+1,qry+q+1);
	int pos=1;
	for(int i=49;i>=0;i--)
	{
		for(int j=31;j>=0;j--)
		{
			f[i][j]=1e10;
		}
	}
	f[0][0]=0;
	sta[0]={0,0,0};
	for(int i=1;i<=q;i++)
	{
		while(pos<=n&&sta[pos].p<qry[i].fi)
		{
			int s1=0,s2=0;
			if(sta[pos].x==2) s1=1;
			if(sta[pos].x==3) s2=1;
			if(sta[pos].x==4) s1=2;
			for(int j=49;j>=0;j--)
			{
				for(int k=31;k>=0;k--)
				{
					f[j][k]=f[j][k]+1.0*(sta[pos].p-sta[pos-1].p)\/mi2[j]\/mi3[k];
					if(j-s1<0||k-s2<0||f[j-s1][k-s2]==1e10) continue;
					f[j][k]=min(f[j][k],f[j-s1][k-s2]+sta[pos].t+1.0*(sta[pos].p-sta[pos-1].p)\/mi2[j-s1]\/mi3[k-s2]);
				}
			}
			pos++;
		}
		double cnt=1e10;
		for(int j=49;j>=0;j--)
		{
			for(int k=31;k>=0;k--)
			{
				if(f[j][k]==1e10) continue;
				cnt=min(cnt,f[j][k]+1.0*(qry[i].fi-sta[pos-1].p)\/mi2[j]\/mi3[k]);
			}
		}
		ans[qry[i].se]=cnt;
	}
	for(int i=1;i<=q;i++) cout<<fixed<<setprecision(9)<<ans[i]<<endl;
	return 0;
}

【MX-S7-T1】「SMOI-R2」Happy Card 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-11-25 14:05:23

题意

给你 $n$ 种类型的纸牌,每种有 $v_i$ 张,你可以出 $4$ 中牌型:单张、对子、炸和三带一。问你最少出多少次可以全出完。

$T$ 组数据,$T \le 10$,$n \le 3 \times 10^5$,$0 \le v_i \le 10^9$。

思路

首先,我们可以把炸拆成三张一样的和另一张一样的,也就是拆成三带一。那么我们就可以先把每个 $v_i$ 拆成若干组一样的和 $1$ 张或 $2$ 张。处理完之后,对于一张或两张,肯定是先与三张的组成三带一节省的步数最多。

如果剩下了 $1$ 张或 $2$ 两张一组的,就当成单张或对子打出去。

如果剩下了三张一组的,那么任意四组可以组成 $3$ 次三带一,花 $3$ 步;任意三组可以组成 $2$ 组三带一和 $1$ 个单张,花 $3$ 步;任意两组可以组成 $1$ 个三带一和 $1$ 个对子,花 $2$ 步;剩下一个可以拆成 $1$ 张单张和对子,花 $2$ 步。

按照如上的顺序来一步一步进行,举例可以证明步数最少。

不懂可结合代码食用。

Code

#include <bits\/stdc++.h>
#define endl '\n'
#define int long long
#define fi first
#define se second
using namespace std;
const int N=2e5+10;
const int inf=0x3f3f3f3f3f3f3f3f;
int n;
int mp[5];
int ans;
int t;
signed main()
{
\/\/	freopen("card.in","r",stdin);
\/\/    freopen("card.out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>t;
	while(t--)
	{
	    ans=0;
	    for(int i=0;i<=4;i++) mp[i]=0;
	    cin>>n;
	    int f1=0,f2=0;
    	for(int i=1;i<=n;i++)
    	{
    	    int a;
    	    cin>>a;
    	    mp[3]+=a\/3;
    	    a%=3;
    	    mp[a]++;
    	    if(a==1) f1++;
    	    if(a==2) f2++;
        }
        int p=min(mp[1],mp[3]);
        ans+=p;
        mp[3]-=p;
        mp[1]-=p;
        p=min(mp[2]*2,mp[3]);
        ans+=p;
        mp[3]-=p;
        mp[2]-=(p+1)\/2;
        mp[1]+=p%2;
        ans+=mp[3]\/4*3;
        mp[3]%=4;
        if(mp[3]==3) ans+=3;
        else if(mp[3]) ans+=2;
        ans+=mp[1]+mp[2];
        cout<<ans<<endl;
    }
    return 0;
}

ABC382-E 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2024-12-01 08:32:46

题意

你有无数包卡牌,每包有 $n$ 张,同时有 $n$ 种稀有卡牌,每张稀有卡牌在每包中出现的概率为 $p_i$,问期望拆多少包卡牌可以获得 $m$ 张稀有卡牌。

$n \le 5000$,$m \le 5000$。

思路

很显然,我们可以用 dp 解决这个问题。

设 $f_i$ 为至少获得 $i$ 张稀有卡牌的期望拆的包数,$g_i$ 为每包恰好获得 $i$ 张卡牌的概率。

我们首先求解 $g$,我们可以考虑新加进来一种稀有卡牌,设新加进来第 $i$ 种稀有卡,考虑转移 $g_j$,那么有两种情况,一种是前 $i-1$ 种卡牌已经拆出 $j$ 张的概率,另一种是前 $i-1$ 种卡牌拆出 $j-1$ 张,同时拆出来了第 $i$ 种的概率,于是转移方程为 $g_j=g_j\times (1-p_i)+g_{j-1}\times p_i$。类似于背包,要倒序转移。

再来求解 $f$。转移时,我们可以枚举这一次拆出来的数量,借助 $g$ 转移。要让总卡牌数至少为 $i$,如果这一包拆出 $j$ 张,则原来至少有 $\max\{0,i-j\}$ 张。方程即为 $f_i=1+\sum_{j=0}^n f_{\max\{0,i-j\}}\times g_j$。

这样还有个问题,$f_i$ 会从自己转移过来,只需要把右边带 $f_i$ 的项移到右边即可。

最终的式子即为 $f_i=\tfrac{1}{1-g_0}\times(1+\sum_{j=1}^n f_{\max\{0,i-j\}}\times g_j)$。

不理解可以结合代码食用。

Code

#include <bits\/stdc++.h>
#define endl '\n'
#define int long long
#define fi first
#define se second
using namespace std;
const int N=5000+10;
const int inf=0x3f3f3f3f3f3f3f3f;
double g[N];
double f[N];
int n,x;
double p[N],ans;
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>n>>x;
	for(int i=1;i<=n;i++)
	{
	    cin>>p[i];
	    p[i]\/=100;
    }
    g[0]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=n;j>=1;j--)
        {
            g[j]=g[j]*(1-p[i])+g[j-1]*p[i];
        }
        g[0]*=(1-p[i]);
    }
    for(int i=1;i<=x;i++)
    {
        f[i]=1;
        for(int j=1;j<=n;j++)
        {
            f[i]+=f[max(0ll,i-j)]*g[j];
        }
        f[i]\/=(1-g[0]);
    }
    printf("%.8lf",f[x]);
    return 0;
}

P5017 [NOIP 2018 普及组] 摆渡车 题解

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2025-02-08 19:26:30

题传

思路

首先,朴素的 dp 转移时容易的。 设 $f_i$ 表示到时刻 $i$,且 $i$ 作为最后一次发车时间的总共的最小代价,然后用前缀和优化一下,令 $cnt_i$ 表示 $i$ 时刻之前的人的数量,$sum_i$ 表示 $i$ 时刻之前所有人的时间之和。

显然有:

$$f_i=\min_{0\le j \le i-m} f_j+i \times (cnt_i-cnt_j) -(sum_i-sum_j)$$

朴素优化

首先,考虑一定不会有一个长于 $m$ 的时间段使得没有任何一班车,因此我们可以将转移的下界设为 $i-2m$,时间复杂度 $O(tm)$。

其次,我们考虑,$t$ 很大,但 $m$ 很小,说明一定存在一些长为 $m$ 的区间一个人都没有,所以对于这样的区间,我们可以直接令 $f_i=f_{i-m}$。

至此,复杂度优化到了 $O(nm^2+t)$,可以通过。

斜率优化

将 dp 转移的式子进行拆分、移项,可得到以下的式子:

$$f_j+sum_j=i\times cnt_j+(f_i-cnt_i\times i+sum_i)$$

与一次函数的解析式 $y=kx+b$ 进行对比,可以发现:

$ y\to f_j+sum_j$

$k\to i$

$x\to cnt_j$

$b\to f_i-cnt_i\times i+sum_i$

也就是说,dp 转移方程是一个一次函数。

可以将前面的每一个状态看作是一个坐标为 $(cnt_j,f_j+sum_j)$ 的点,那么,在每次转移的过程中,我们就是要找到一个点,使得经过这个点的、斜率为 $i$ 的直线与 $y$ 轴的交点的纵坐标最小。

那么,其实就是求与前面所有的状态表示的点组成的凸包相切的直线。

由于求的是最小值,于是只维护下凸包即可。

可以用单调栈维护凸包。

由于斜率单调递增,于是可以使用单调队列,每次把不符合要求的点直接剔除。

时间复杂度 $O(t)$。

Code

#include <bits\/stdc++.h>
#define endl '\n'
#define int long long
#define fi first
#define se second
using namespace std;
const int N=4e6+100+10;
const int inf=0x3f3f3f3f3f3f3f3f;
pair<int,int> q[N];
int n,m;
int t;
int cnt[N];
int sum[N];
int f[N];
int l,r;
double slope(pair<int,int> x,pair<int,int> y)
{
    if(x.fi==y.fi) return (x.se>y.se?-inf:inf);
    return (double)(y.se-x.se)\/(double)(y.fi-x.fi);
}
int ans=inf;
signed main()
{
	\/\/freopen(".in","r",stdin);
    \/\/freopen(".out","w",stdout);
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(false);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
	    int x;
	    cin>>x;
	    cnt[x]++;
	    sum[x]+=x;
	    t=max(t,x);
    }
    for(int i=1;i<=t+m;i++) sum[i]+=sum[i-1],cnt[i]+=cnt[i-1];
    l=1;
    for(int i=0;i<=m-1;i++) f[i]=i*cnt[i]-sum[i];
    for(int i=m;i<t+m;i++)
    {
        while(l<r&&slope(q[r-1],q[r])>slope(q[r],{cnt[i-m],f[i-m]+sum[i-m]})) r--;
        q[++r]={cnt[i-m],f[i-m]+sum[i-m]};
        while(l<r&&slope(q[l],q[l+1])<i) l++;
        f[i]=q[l].se-i*q[l].fi+i*cnt[i]-sum[i];
    }
    for(int i=t;i<t+m;i++) ans=min(ans,f[i]);
    cout<<ans;
    return 0;
}

做题记录 十二重计数法

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2025-02-11 09:23:08

$I$

显然 $m^n$。

$II$

让每个球去选盒子,显然为 $\mathcal{A}_m^n$。

$III$

与第二类斯特林数有关

式子为 $${n \brace m} m!$$。

但是,第二类斯特林数的递推式子是 $O(nm)$ 的,因此我们需要一些优化。

法一:容斥

考虑总数为 $m^n$ 然后考虑排除不合法的方案数。

我们可以固定一些盒子,让他们强制为空。

根据容斥的公式,那么不合法的,也就是有空盒的方案数即为

$$\sum_{k=1}^{m} (-1)^{k-1} {m\choose k}(m-k)^n$$

那么,我们可以得到,原问题的方案数可以不用第二类斯特林数,可以用容斥得到。

$$m^n-\sum_{k=1}^{m} (-1)^{k-1} {m\choose k}(m-k)^n$$ $$=\sum_{k=0}^{m}(-1)^{k} {m\choose k}(m-k)^n$$

完事

法二:

考虑总的方案数为枚举哪些盒子为非空

$$m^n= \sum_{k=0}^m{n \choose k}{n \brace k}k!$$

直接二项式反演

$${n \brace m}m!=\sum_{k=0}^{m}(-1)^{k} {m\choose k}(m-k)^n$$

$IV$

枚举有多少盒子非空。

$$\sum_{i=1}^{m} {n\brace i}$$

$V$

搞笑的,$[n\le m]$。

$VI$

第二类斯特林数的定义

可通过 $III$ 得到。

$VII$

隔板,$n+m-1\choose m-1$。

$VIII$

考虑选出哪些盒子装球,$m\choose n$

$IX$

隔板 $n-1\choose m-1$。

$X$

考虑 dp,设 $f_{i,j}$ 表示 $i$ 个球,$j$ 个盒子的方案数。

考虑新增一个盒子的方案数。

如果必须有空盒,那就加上一个空盒;不能有空盒,那就把已有的全部 $+1$.

$f_{i,j}=f_{i,j-1}+f_{i-j,j}$。

但是,这是 $O(nm)$ 的。

所以,我们要上多项式优化。

构造多项式 $F_i(x)=(f_{i,0}+f_{i,1}x+f_{i,2}x^2+f_{i,3}x^3+...)$

根据转移方程,则有

$F_i(x)=F_{i-1}(x)\times (1+x^i+x^{2i}+...)$

又根据生成函数

有 $F_i(x)=\frac{F_{i-1}(x)}{1-x^i}$

即 $$F_i(x)=\prod_{j=1}^i\frac{1}{1-x^j}$$

然后对这个东西取 $ln$,加回去再取 $exp$ 即可。

详见付公主的背包 (不会,暂时贺上)

$XI$

搞笑的,$[n\le m]$

$XII$

先令每个盒子里有一个,转化成了 $X$,答案是 $f_{n-m,m}$。

关于多项式的部分,先欠着。

ABC397 做题记录

本文章由 WyOJ Shojo 从洛谷专栏拉取,原发布时间为 2025-03-17 14:42:30

一场极其抽象的 ABC

A

没什么好说的,直接过了

B

直接过了

C

正着来一遍,倒着来一遍,完事

D

全场最大败笔。

在场上,我首先想到枚举其中一个数,然后算另一个。

发现在两个数差 $1$ 的情况下,小的那个数不会超过 $4\times 10^8$ 。

然后喜提 TLE

然后想到 $a^3-b^3=(a-b)(a^2+ab+b^2)=n$

这样可以枚举 $(a-b)$,是 $sqrt(n)$ 的。

又喜提 TLE

扔到自定义测试上,发现 $1\times 10^{18}$ 跑了不到 3s

尝试卡常,未果

此时已经将近 $21:10$,果断弃掉。

赛后炜哥讲了他的枚举两个数的差,再二分的做法,突然意识到我这个枚举的也是差,而差不可能大于 $1\times 10^6$

糖丸了

E

$21:28$ 开题,$21:30$ 想出枚举叶子往上缩。

但是没写完。

赛后写完,发现挂了,先睡觉。

早上调到只 WA 最后两个

然后发现 $k=1$ 时没特判...

F

扫了一眼 E,F 发现 F 好像好写,于是先写 F

发现只要知道断点,直接做完了

发现可以 $O(n^2)$ dp

遂想到扔到线段树上,枚举第二个断点 $i$。线段树的每一个点表示以每个点为第一段结尾、第二段结尾在 $i$ 权值。

每个数只会在 $la$ 到 $i-1$ 有贡献。

最后一段的贡献可以预处理。

做完了。

总结

看了榜1的 D,发现我的最开始的枚举也可以,只是加一个双指针就行,直接做还是太劣了。

炜哥的 E 写的跟我不太一样,但是大差不差,是按树高向上做,挺对的,还短好多,没有我那么多特判。

F 扫了一眼基本上全是一样的写法。

还是要多注意一些小细节,避免悲剧的再次发生。

共 35 篇博客