Petrozavodsk Winter 2023. Day 1 部分题解

文章探讨了在给定的二分图上寻找最大权匹配的问题,并在保持最大权的情况下,如何找到字典序最小的匹配方案。通过构造映射和使用KM算法,可以在O(m^3)的时间复杂度内找到全局最优解。此外,还讨论了一个涉及分数排名变化的动态规划问题,利用树状数组维护每个人的分数和贡献,实现了对操作序列的高效处理。
摘要由CSDN通过智能技术生成

前言:整场的题目质量比较高,虽然之前做过一部分题,但还是被薄纱了

Changing the Sequences

大意:
给定两个数组a,b,长度都为n,元素都介于1-m之间

定义一次操作如下:

构造一个1-m的排列p,对于对于a中的每一个元素,a_i=p_{a_i},得到a'

只进行一次该操作,要求使a'与b的元素不相同的位置尽可能少。并求出满足条件的字典序最小的a'

思路:

显然一次操作的本质是构造一个a到a'的单射。我们可以直接考虑映射的值。将ai与对应的bi连边,表示如果ai的映射是bi,可以造成一个贡献。我们希望最终的贡献尽可能多。显然边的权值可以累加。

所以本质上我们就是在一个二分图上寻找匹配,使得匹配边的权值之和尽可能大。也就是最大权匹配问题。如果不考虑字典序最小的话,我们可以直接用KM来解决这个问题。时间复杂度就是m^3

现在想想如何让字典序最小。说来可以,这其实是一种不算罕见的套路,但是赛时我们队还是全体宕机,但事后看看,并没有想象中的那么不可做。

显然字典序最小的前提是保持权值和最大,然后我们去修改可以连边,使得a数组中靠前的数字尽可能去匹配尽可能小的数字。为了做到这一点,我们先将a数组的元素按位置先后从小到大赋值(因为其本身的元素大小并没有什么用,我们关心的只是元素的位置)。

然后无脑跑一遍KM,得到最大的匹配权值和ans。然后我们尝试修改匹配方案。

外层遍历更新后的a数组,内层遍历映射的域,分别记为i,j。尝试将i的映射变成j,我们需要验证i->j的权值贡献加上图的其他部分的贡献与ans相同。那么将i连向其他点的权值置为0,再将其他点连向j的权值置为0,重新跑一遍KM,加上i到j的权值贡献,即可。如果和与ans相同的话,代表这个匹配是可以的。这里有贪心的成分在,但是因为我们是要字典序最小,前缀越小约好,所以可以贪心。匹配成功后,我们就将两个点从图中去除即可。否则恢复成原来的状态。

code

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define endl '\n'
const int INF=0x3f3f3f3f;
const int N=1e5+10;

int n,m,match[N],pre[N];
bool vis[N];
int favor[70][70];
int val1[N],val2[N],slack[N];
ll a[N],b[N];
ll mp[N],cn;
ll To[70];
ll Pre[70][70];
ll change[70];
ll VIS[70];

void bfs(int p)
{
    memset(pre,0,sizeof pre);
    memset(slack,INF,sizeof slack);

    match[0]=p;

    int x=0,nex=0;
    do{
        vis[x]=true;

        int y=match[x],d=INF;

        // 瀵逛簬褰撳墠鑺傜偣y锛宐fs鏈夎繛杈圭殑涓嬩竴鐐?
        for(int i=1;i<=m;i++)
        {
            if(!vis[i])
            {
                if(slack[i]>val1[y]+val2[i]-favor[y][i])
                {
                    slack[i]=val1[y]+val2[i]-favor[y][i];
                    pre[i]=x;
                }
                if(slack[i]<d)
                {
                    d=slack[i];
                    nex=i;
                }
            }
        }

        for(int i=0;i<=m;i++)
        {
            if(vis[i])
                val1[match[i]]-=d,val2[i]+=d;
            else
                slack[i]-=d;
        }

        x=nex;

    }while(match[x]);

    while(x)
    {
        match[x]=match[pre[x]];
        x=pre[x];
    }
}

ll KM()
{
    memset(match,0,sizeof match);
    memset(val1,0,sizeof val1);
    memset(val2,0,sizeof val2);
    for(int i=1;i<=m;i++)
    {
        memset(vis,false,sizeof vis);
        bfs(i);
    }
    ll ans=0;
    for(int i=1;i<=m;++i) ans+=favor[match[i]][i];
    	return ans;
}
int main()
{
	cin>>n>>m;
	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)
	{
		if(!mp[a[i]]) mp[a[i]]=++cn;
		a[i]=mp[a[i]];
	}

	for(int i=1;i<=n;++i)
	{
		favor[a[i]][b[i]]++;
	}
	for(int i=1;i<=m;++i)
	{
		for(int j=1;j<=m;++j) Pre[i][j]=favor[i][j];//原始数组
	}

	ll ans=KM();//全局最优解

	ll sum=0;
	for(int i=1;i<=m;++i)
	{
		for(int j=1;j<=m;++j)
		{
			if(VIS[j]) continue;
			ll th_val=favor[i][j];
			for(int s=1;s<=m;++s)
			{
				favor[i][s]=0;
				favor[s][j]=0;//除去
			}
			if(th_val+sum+KM()==ans)//代表匹配成功,我们修改它
			{
				VIS[j]=1;
				change[i]=j;
				sum+=th_val;//sum记录之前已经匹配过的映射的权值贡献和
				break;
			}
			for(int s=1;s<=m;++s)
			{
				if(s>=i) favor[s][j]=Pre[s][j];//前面都是已匹配过的点
				if(VIS[s]) continue;//已经连接过
				favor[i][s]=Pre[i][s];
			}

		}
	}
	for(int i=1;i<=n;++i) cout<<change[a[i]]<<' ';

	return 0;
}

Determine The Fluctuation Bonus

大意:

给定n个人,每一个人有一个初始分数为0.q次操作,每次操作给定id,a,第id个人加上a分。每次操作之后所有人按分数重新排序,获得排名变化的绝对值的贡献。

求q次操作之后每一个人的贡献。

思路:

看着难,实际也没那么不可做,还是太怂了

每一个人的分数可以分成两个部分:自己操作带来的贡献,以及别人操作对自己带来的贡献。第一个值其实比较好维护,我们只需要记录每一个人的分数,然后每一次操作的时候维护一下排名,贡献就是排名的变化了,具体实现用一个树状数组T1来维护分数。第二个部分可以用另一个树状数组T2维护。每一次操作之后,分数从pre变成了now,那么只有分数区间在[pre,now-1]之间的人才会获得贡献,并且贡献为1,简单做一个差分即可。

最后一个问题就是如何将两者相加。让第二个树状数组T2记录每一个分数累计获得的贡献。每次操作的时候,对于id,我们然其贡献加上T2(pre),再减去T2(now),即可。因为id之前一直在pre的位置,所以加上这个位置带来的贡献。但是下一次又轮到id操作的时候,它并不是从一开始就待在当前的now这个位置的,所以我们先提前减去T2(now),下一次就可以放心加上对应的值了,实现起来也比较容易。

同时数据值域比较大,我们还要提前离线下来把数据离散化。

写起来有点绕,细节比较多。

code

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define low(x) x&(-x)
#define endl '\n'
const ll N=2e5+10;
const ll M=2e5;
struct Tree
{
	ll tr[N];
	void add(ll x,ll y)
	{
		while(x<=M)
		{
			tr[x]+=y;
			x+=low(x);
		}
	}
	ll sum(ll x)
	{
		ll ans=0;
		while(x)
		{
			ans+=tr[x];
			x-=low(x);
		}
		return ans;
	}
	ll Gt_sum(ll l,ll r)
	{
		return sum(r)-sum(l-1);
	}
	void upd(ll l,ll r,ll val)
	{
		add(l,val);add(r+1,-val);
	}
}T1,T2;
ll n,q;
map<ll,ll> mp;
struct ty
{
	ll id,val;
}mas[N];
ll pos[N];
set<ll> st;
int idx=0;
ll ans[N];
void solve()
{
	cin>>n>>q;
	st.insert(0);
	for(int i=1;i<=q;++i)
	{
		cin>>mas[i].id>>mas[i].val;
		st.insert(pos[mas[i].id]);
		pos[mas[i].id]+=mas[i].val;
		st.insert(pos[mas[i].id]);
	}
	for(auto s:st) mp[s]=++idx;
	for(int i=0;i<=n;++i) pos[i]=0;

	// for(auto s:st) cout<<s<<" ";
	// 	cout<<endl;

	T2.add(mp[0],n);
	for(int i=1;i<=q;++i)
	{
		ll id=mas[i].id;ll val=mas[i].val;
		ll pre_pos=mp[pos[id]],now_pos=mp[pos[id]+val];
		pos[id]+=val;

		ll pre_lnk=1+T2.Gt_sum(pre_pos+1,M);
		// T2.upd(pre_pos,pre_pos,-1);T2.upd(now_pos,now_pos,1);
		T2.add(pre_pos,-1);T2.add(now_pos,1);
		ll now_lnk=1+T2.Gt_sum(now_pos+1,M);
		// cout<<id<<" "<<pre_lnk<<' '<<now_lnk<<endl;

		ans[id]+=abs(pre_lnk-now_lnk);
		ans[id]+=T1.sum(pre_pos);
		ll mi=min(pre_pos,now_pos);ll ma=max(pre_pos,now_pos)-1;
		T1.upd(mi,ma,1);
		ans[id]-=T1.sum(now_pos);
	}
	// for(int i=1;i<=n;++i) cout<<ans[i]<<" ";
	// 	cout<<endl;
	for(int i=1;i<=n;++i)
	{
		cout<<ans[i]+T1.sum(mp[pos[i]])<<endl;
	}

}
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	solve();
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值