2023年团体程序设计天梯赛 题解

Posted HEY

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2023年团体程序设计天梯赛 题解相关的知识,希望对你有一定的参考价值。

2023年团体程序设计天梯赛

仅更新L1,L2随后写

L1-1 最好的文档

点击查看本题

有一位软件工程师说过一句很有道理的话:“Good code is its own best documentation.”(好代码本身就是最好的文档)。本题就请你直接在屏幕上输出这句话。

输入格式:

本题没有输入。

输出格式:

在一行中输出Good code is its own best documentation.

输入样例:

输出样例:

Good code is its own best documentation.

思路

直接输出

代码

#include <iostream>

using namespace std;

int main()

    cout << "Good code is its own best documentation." << endl;
    return 0;

L1-2 什么是机器学习

点击查看本题


什么是机器学习?上图展示了一段面试官与“机器学习程序”的对话:

面试官:9 + 10 等于多少?
答:3
面试官:差远了,是19。
答:16
面试官:错了,是19。
答:18
面试官:不,是19。
答:19

本题就请你模仿这个“机器学习程序”的行为。

输入格式:

输入在一行中给出两个整数,绝对值都不超过 100,中间用一个空格分开,分别表示面试官给出的两个数字 A 和 B。

输出格式:

要求你输出 4 行,每行一个数字。第 1 行比正确结果少 16,第 2 行少 3,第 3 行少 1,最后一行才输出 A+B 的正确结果。

输入样例:

9 10

输出样例:

3
16
18
19

思路

先计算a+b的总和sum,然后第一行输出sum-16,第二行输出sum-3,第三行输出sum-1,第四行输出sum

代码

#include <iostream>

using namespace std;

int main()

    int a , b;
    cin >> a >> b;
    
    int sum = a + b;
    
    cout << sum-16 << endl;
    cout << sum-3 << endl;
    cout << sum-1 << endl;
    cout << sum << endl;
    
    return 0;

L1-3 程序员买包子

点击查看本题


这是一条检测真正程序员的段子:假如你被家人要求下班顺路买十只包子,如果看到卖西瓜的,买一只。那么你会在什么情况下只买一只包子回家?
本题要求你考虑这个段子的通用版:假如你被要求下班顺路买 N 只包子,如果看到卖 X 的,买 M 只。那么如果你最后买了 K 只包子回家,说明你看到卖 X 的没有呢?

输入格式:

输入在一行中顺序给出题面中的 N、X、M、K,以空格分隔。其中 N、M 和 K 为不超过 1000 的正整数,X 是一个长度不超过 10 的、仅由小写英文字母组成的字符串。题目保证 N!=M。

输出格式:

在一行中输出结论,格式为:
如果 K=N,输出 mei you mai X de;
如果 K=M,输出 kan dao le mai X de;
否则输出 wang le zhao mai X de.
其中 X 是输入中给定的字符串 X。

输入样例 1:

10 xigua 1 10

输出样例 1:

mei you mai xigua de

输入样例 2:

10 huanggua 1 1

输出样例 2:

kan dao le mai huanggua de

输入样例 3:

10 shagua 1 250

输出样例 3:

wang le zhao mai shagua de

思路

读入 N、X、M、K后,对k和n或m进行判断,然后输出,if else ......

代码

#include <iostream>

using namespace std;

int main()

    int n ,m , k;
    string x;
    cin >> n >> x >> m >> k;
    
    if(k == n)
        cout << "mei you mai " << x << " de" << endl;
    else if(k == m)
       cout << "kan dao le mai " << x << " de" << endl;
    else
        cout << "wang le zhao mai " << x << " de" << endl;
    
    return 0;

L1-4 进化论

点击查看本题


在“一年一度喜剧大赛”上有一部作品《进化论》,讲的是动物园两只猩猩进化的故事。猩猩吕严说自己已经进化了 9 年了,因为“三年又三年”。猩猩土豆指出“三年又三年是六年呐”……
本题给定两个数字,以及用这两个数字计算的结果,要求你根据结果判断,这是吕严算出来的,还是土豆算出来的。

输入格式:

输入第一行给出一个正整数 N,随后 N 行,每行给出三个正整数 A、B 和 C。其中 C 不超过 10000,其他三个数字都不超过 100。

输出格式:

对每一行给出的三个数,如果 C 是 A×B,就在一行中输出 Lv Yan;如果是 A+B,就在一行中输出 Tu Dou;如果都不是,就在一行中输出 zhe du shi sha ya!。

输入样例:

3
3 3 9
3 3 6
3 3 12

输出样例:

Lv Yan
Tu Dou
zhe du shi sha ya!

思路

有T组数据,输入a,b,c后,判断c=a*b还是c=a+b还是什么也不是,按照题意输出

代码

#include <iostream>

using namespace std;

void solve()

    int a , b , c;
    cin >> a >> b >> c;
    
    if(c == a*b)
        cout << "Lv Yan" << endl;
    else if(c == a+b)
        cout << "Tu Dou" << endl;
    else
        cout << "zhe du shi sha ya!" << endl;
    


int main()

    int T;
    cin >> T;
    
    while(T--)
        solve();
    
    return 0;

L1-5 猜帽子游戏

点击查看本题


宝宝们在一起玩一个猜帽子游戏。每人头上被扣了一顶帽子,有的是黑色的,有的是黄色的。每个人可以看到别人头上的帽子,但是看不到自己的。游戏开始后,每个人可以猜自己头上的帽子是什么颜色,或者可以弃权不猜。如果没有一个人猜错、并且至少有一个人猜对了,那么所有的宝宝共同获得一个大奖。如果所有人都不猜,或者只要有一个人猜错了,所有宝宝就都没有奖。
下面顺序给出一排帽子的颜色,假设每一群宝宝来玩的时候,都是按照这个顺序发帽子的。然后给出每一群宝宝们猜的结果,请你判断他们能不能得大奖。

输入格式:

输入首先在一行中给出一个正整数 N(2<N≤100),是帽子的个数。第二行给出 N 顶帽子的颜色,数字 1 表示黑色,2 表示黄色。
再下面给出一个正整数 K(≤10),随后 K 行,每行给出一群宝宝们猜的结果,除了仍然用数字 1 表示黑色、2 表示黄色之外,0 表示这个宝宝弃权不猜。
同一行中的数字用空格分隔。

输出格式:

对于每一群玩游戏的宝宝,如果他们能获得大奖,就在一行中输出 Da Jiang!!!,否则输出 Ai Ya。

输入样例:

5
1 1 2 1 2
3
0 1 2 0 0
0 0 0 0 0
1 2 2 0 2

输出样例:

Da Jiang!!!
Ai Ya
Ai Ya

思路

先读入每顶帽子的颜色,然后进行T组判断:

  • 如果为0,则表示不猜,ling++
  • 如果与当前颜色相同 dui++
  • 所猜的颜色不同 cuo++

现在已知在本次猜的过程中:不猜的人数,猜错的人数,猜对的人数
由题意可知:

  • 如果没有一个人猜错、并且至少有一个人猜对了,那么所有的宝宝共同获得一个大奖。
    即如果 cuo=0 并且 dui>=1 输出Da Jiang!!!
  • 其他结果输出 Ai Ya

代码

#include <iostream>

using namespace std;

const int N = 110;

int n , T, x;
int a[N];

void solve()

    int dui = 0;
    int ling = 0;
    int cuo = 0;
    
    for(int i = 0;i < n;i ++)
    
        cin >> x;
        
        if(x == 0)
            ling ++;
        else if(x == a[i])
            dui ++;
        else cuo ++;
    
    
    if(!cuo && dui)
        puts("Da Jiang!!!");
    else
        puts("Ai Ya");


int main()

    cin >> n;
    
    for(int i = 0;i < n;i ++)
        cin >> a[i];
    
    cin >> T;
    
    while(T--)
        solve();
    
    return 0;

L1-6 剪切粘贴

点击查看本题

使用计算机进行文本编辑时常见的功能是剪切功能(快捷键:Ctrl + X)。请实现一个简单的具有剪切和粘贴功能的文本编辑工具。

工具需要完成一系列剪切后粘贴的操作,每次操作分为两步:

  • 剪切:给定需操作的起始位置和结束位置,将当前字符串中起始位置到结束位置部分的字符串放入剪贴板中,并删除当前字符串对应位置的内容。例如,当前字符串为 abcdefg,起始位置为 3,结束位置为 5,则剪贴操作后, 剪贴板内容为 cde,操作后字符串变为 abfg。字符串位置从 1 开始编号。
  • 粘贴:给定插入位置的前后字符串,寻找到插入位置,将剪贴板内容插入到位置中,并清除剪贴板内容。例如,对于上面操作后的结果,给定插入位置前为 bf,插入位置后为 g,则插入后变为 abfcdeg。如找不到应该插入的位置,则直接将插入位置设置为字符串最后,仍然完成插入操作。查找字符串时区分大小写。
    每次操作后的字符串即为新的当前字符串。在若干次操作后,请给出最后的编辑结果。

输入格式:

输入第一行是一个长度小于等于 200 的字符串 S,表示原始字符串。字符串只包含所有可见 ASCII 字符,不包含回车与空格。

第二行是一个正整数 N (1≤N≤100),表示要进行的操作次数。

接下来的 N 行,每行是两个数字和两个长度不大于 5 的不包含空格的非空字符串,前两个数字表示需要剪切的位置,后两个字符串表示插入位置前和后的字符串,用一个空格隔开。如果有多个可插入的位置,选择最靠近当前操作字符串开头的一个。

剪切的位置保证总是合法的。

输出格式:

输出一行,表示操作后的字符串。

输入样例:

AcrosstheGreatWall,wecanreacheverycornerintheworld
5
10 18 ery cor
32 40 , we
1 6 tW all
14 18 rnerr eache
1 1 e r

输出样例:

he,allcornetrrwecaneacheveryGreatWintheworldAcross

思路

先读入所需要修改的字符串str,然后进行T组数据:
1.读入剪切的位置l,r ; 需要粘贴的两段字符串tmp1,tmp2。注意:题目中字符串的位置从1开始,所以l,r需要-1
2.提取出剪切的字符串res,然后更新剪切后的字符串ans。ans = str.substr(0,l) + str.substr(r+1);
3.将tmp1和tmp2进行拼接,便于寻找粘贴的位置
4.利用双指针(应该是)找粘贴的位置,i来指向需要修改的字符串,j指向粘贴的那个拼接字符串

	while(i < ans.length() && j < tmp.length())
	
		if(ans[i] == tmp[j])
			j++;
		else
			j = 0;
		i++;
	

5.另一个大坑:如找不到应该插入的位置,则直接将插入位置设置为字符串最后,仍然完成插入操作。查找字符串时区分大小写。

代码

#include <bits/stdc++.h>

using namespace std;

string str , ans , res;
int T;

void solve()

    int l , r;
    string tmp1 , tmp2;
    
    scanf("%d %d ",&l,&r); 
	cin >> tmp1 >> tmp2;
    
 //   cout << l << " " << r << " " << tmp1 << " " << tmp2 << endl;
	
	l -- ,r --;
	
	res = str.substr(l,r-l+1);
//	cout << "Res = " << res << endl;
	ans = str.substr(0,l) + str.substr(r+1);
//	cout << "Ans = " << ans << endl;

	string tmp = tmp1 + tmp2;
//	cout << "Tmp = " << tmp << endl;
	int i = 0,j = 0,k = tmp2.length();
	
	while(i < ans.length() && j < tmp.length())
	
		if(ans[i] == tmp[j])
			j++;
		else
			j = 0;
		i++;
	
	
//	cout << "I = " << i << endl;
	
	if(j == tmp.length()) // 找到了 
	
		str = ans.substr(0,i-k) + res + ans.substr(i-k);
	
	else // 没找到 
	
		str = ans + res;
	
	
//	cout << "Str = " << str << endl;



int main()

    getline(cin,str);
    
    cin >> T;
    
    while(T--)
        solve();
    
    cout << str << endl;
    return 0;

L1-7 分寝室

点击查看本题

学校新建了宿舍楼,共有 n 间寝室。等待分配的学生中,有女生 n0 位、男生 n1位。所有待分配的学生都必须分到一间寝室。所有的寝室都要分出去,最后不能有寝室留空。
现请你写程序完成寝室的自动分配。分配规则如下:

  • 男女生不能混住;
  • 不允许单人住一间寝室;
  • 对每种性别的学生,每间寝室入住的人数都必须相同;例如不能出现一部分寝室住 2 位女生,一部分寝室住 3 位女生的情况。但女生寝室都是 2 人一间,男生寝室都是 3 人一间,则是允许的;

在有多种分配方案满足前面三项要求的情况下,要求两种性别每间寝室入住的人数差最小。

输入格式:

输入在一行中给出 3 个正整数 n0、n1、n,分别对应女生人数、男生人数、寝室数。数字间以空格分隔,均不超过 10^5。

输出格式:

在一行中顺序输出女生和男生被分配的寝室数量,其间以 1 个空格分隔。行首尾不得有多余空格。
如果有解,题目保证解是唯一的。如果无解,则在一行中输出 No Solution。

输入样例 1:

24 60 10

输出样例 1:

4 6

注意:输出的方案对应女生都是 24/4=6 人间、男生都是 60/6=10 人间,人数差为 4。满足前三项要求的分配方案还有两种,即女生 6 间(都是 4 人间)、男生 4 间(都是 15 人间);或女生 8 间(都是 3 人间)、男生 2 间(都是 30 人间)。但因为人数差都大于 4 而不被采用。

输入样例 2:

29 30 10

输出样例 2:

No Solution

思路

寝室数最大为10^5,所以可以遍历一遍,遍历的条件:女寝为1,男寝为n-1时......
遍历的过程中:

  • 先判断寝室数是否能满足每间寝室入住的人数都必须相同,即n0%i == 0 && n1%j == 0
  • 然后进行判断,如果是第一次就直接复制,否则进行判断当前i,j是否最优(两种性别每间寝室入住的人数差最小)

1分的测试点

题目中要求:不允许单人住一间寝室;
因此在最后判断一下,如果有单人住一间寝室的话,输出No Solution

代码

#include <iostream>

using namespace std;

int main()

    int n0,n1,n;
    
    cin >> n0 >> n1 >> n;
    
    bool flag = false;
    int ansi , ansj;
    
    for(int i = 1;i < n;i ++)
    
        int j = n-i;
        
        if(n0%i == 0 && n1%j == 0)
        
            if(!flag || (abs(n0/i - n1/j) < abs(n0/ansi - n1/ansj)) )
            
                ansi = i;
                ansj = j;
                flag = true;
            
        
        
    
    
    if(!flag || n0/ansi == 1 || n1/ansj == 1)
        cout << "No Solution" << endl;
    else
        cout << ansi << " " << ansj << endl;
    
    return 0;

L1-8 谁管谁叫爹

点击查看本题


《咱俩谁管谁叫爹》是网上一首搞笑饶舌歌曲,来源于东北酒桌上的助兴游戏。现在我们把这个游戏的难度拔高一点,多耗一些智商。
不妨设游戏中的两个人为 A 和 B。游戏开始后,两人同时报出两个整数 NA和 N B。判断谁是爹的标准如下:
将两个整数的各位数字分别相加,得到两个和 SA 和 SB。如果 NA正好是 SB的整数倍,则 A 是爹;如果 NB 正好是 SA 的整数倍,则 B 是爹;
如果两人同时满足、或同时不满足上述判定条件,则原始数字大的那个是爹。
本题就请你写一个自动裁判程序,判定谁是爹。

输入格式:

输入第一行给出一个正整数 N(≤100),为游戏的次数。以下 N 行,每行给出一对不超过 9 位数的正整数,对应 A 和 B 给出的原始数字。题目保证两个数字不相等。

输出格式:

对每一轮游戏,在一行中给出赢得“爹”称号的玩家(A 或 B)。

输入样例:

4
999999999 891
78250 3859
267537 52654299
6666 120

输出样例:

B
A
B
A

思路

T组数据,读入na,nb,然后根据clac()函数计算出sa,sb。然后根据题意进行判断:

  • 如果NA正好是SB的整数倍,并且NB不是SA的整数倍 , A赢
  • 如果NB正好是SA的整数倍,并且NA不是SB的整数倍 , B赢
  • 如果两个都满足,或都不满足,判断na和nb的大小,谁大谁赢

代码

#include <iostream>

using namespace std;

int clac(int u)

    int sum = 0;
    
    while(u)
    
        sum += u % 10;
        u /= 10;
    
    
    return sum;

    
void solve()

    int na , nb;
    cin >> na >> nb;
    
    int sa = clac(na);
    int sb = clac(nb);
    
    bool flag1 = (na%sb==0);
    bool flag2 = (nb%sa==0);
    
    if(flag1 && !flag2)
        puts("A");
    else if(!flag1 && flag2)
        puts("B");
    else if(na > nb)
        puts("A");
    else
        puts("B");
        


int main()

    int T;
    cin >> T;
    
    while(T--)
        solve();
    
    return 0;

正睿2018暑假集训 比赛题选做

题解 ZR246 数对子

题目大意

我们定义一个数对((x,y))是好的,当且仅当(x≤y),且(xoperatorname{xor} y)的二进制表示下有奇数个(1)

现在给定(n)个区间([l_i,r_i]),你需要对于每个(iin[1,n]),输出有几对好的数((x,y))满足(x)(y)都在 ([l_1,r_1]cup[l_2,r_2]dotscup[l_i,r_i]),即两个数都在前(i)个区间的并里。

数据范围:(1leq nleq 10^5), (1leq l_ileq r_ileq 2^{32}-1)

本题题解

异或有一个特性:两个数异或,带来的(二进制下)(1)的数量的变化,永远是偶数个:要么不变、要么减少(2)

由此可知:(xoperatorname{XOR} y)有奇数个(1),当且仅当(x), (y)中一个是奇数,一个是偶数。

设所有数中(1)的数量为偶数的数有( ext{cnt0})个,(1)的数量为奇数的数有( ext{cnt1})个。则答案就是( ext{cnt0} imes ext{cnt1})

我们要支持插入线段、维护( ext{cnt0})( ext{cnt1})。可以用线段树实现。

建一个动态开点线段树,值域为([0,2^{32}-1])。则线段树上每个节点,都表示了一段形如([a2^b,(a+1)2^b-1])的区间,这样的区间(长度等于(1)的除外)里,( ext{cnt0})( ext{cnt1})永远是相等的:都等于区间长度的一半。这就很方便我们用线段树维护我们需要的信息。

时间复杂度(O(nlog n))

参考代码:

//problem:ZR246
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;

const int MAXN=1e5;
const ll MAXM=(1LL<<32)-1;

int bitcnt(ll x){
	int res=0;
	while(x){
		res+=(x&1LL);
		x>>=1;
	}
	return res;
}
pll operator+(pll x,pll y){
	return mk(x.fi+y.fi,x.se+y.se);
}
void operator+=(pll& x,pll y){
	x=x+y;
}

int rt;
struct SegmentTree{
	static const int SIZE=MAXN*80;
	int ls[SIZE+5],rs[SIZE+5],cov[SIZE+5],tot;
	ll cnt0[SIZE+5],cnt1[SIZE+5];
	void upd(int p,ll l,ll r){
		if(cov[p]){
			if(l==r){
				cnt0[p]=cnt1[p]=0;
				if(bitcnt(l)&1)cnt1[p]=1;
				else cnt0[p]=1;
			}
			else{
				cnt0[p]=cnt1[p]=(r-l+1)/2;
			}
		}
		else{
			if(l==r){
				cnt0[p]=cnt1[p]=0;
			}
			else{
				cnt0[p]=cnt0[ls[p]]+cnt0[rs[p]];
				cnt1[p]=cnt1[ls[p]]+cnt1[rs[p]];
			}
		}
	}
	void modify(int& p,ll l,ll r,ll ql,ll qr,int x){
		if(!p)p=++tot;
		if(ql<=l&&qr>=r){
			cov[p]+=x;
			upd(p,l,r);
			return;
		}
		ll mid=(l+r)>>1;
		if(ql<=mid)modify(ls[p],l,mid,ql,qr,x);
		if(qr>mid)modify(rs[p],mid+1,r,ql,qr,x);
		upd(p,l,r);
	}
	SegmentTree(){}
}T;

int main() {
	int n;cin>>n;while(n--){
		ll l,r;cin>>l>>r;
		T.modify(rt,0,MAXM,l,r,1);
		cout<<T.cnt0[rt]*T.cnt1[rt]<<endl;
	}
	return 0;
}

题解 ZR247 逆序对

题目大意

给定一个偶数(N),现在蔡老板得到了一个由([1,N])内的所有偶数构成的排列(b[1..N/2])

现在蔡老板又得到了一个数组(a[1..N/2]),其中(a[i]=icdot 2?1)

蔡老板想知道,对于所有满足(a)(b)都是它的子序列的([1,N])的排列(p)(p)的逆序对数的最小值。

数据范围:(1leq Nleq 2 imes 10^5)

本题题解

逆序对数=偶数序列自身的逆序对数+奇数序列中每个数插入在偶数序列某个位置时产生的贡献。具体来说,插入在偶数序列某个位置时,产生的贡献就是偶数序列中该位置前面大于该奇数的数的数量+后面小于该奇数的数的数量。

考虑从小到大依次把每个奇数插入。每次都从对于该奇数最优的位置插入。这样的逆序对数一定是最少的。但是会不会出现:较小的奇数插在了较后面的位置,较大的奇数插在了较前面的位置,从而导致不满足“奇数序列是结果序列的一个子序列”这一要求呢?并不会。因为如果每次都选择最优的位置,会发现最优的插入位置是单调递增(可能非严格,但是没关系)的。

具体来说,共有(frac{n}{2}+1)个可能的插入位置。我们用线段树维护每个插入位置的代价。从小到大考虑每个奇数。插入完一个奇数后,变成下一个奇数时,需要对线段树做的修改是:让一段后缀(-1),让一段前缀(+1):这样显然最优的位置单调递增。

时间复杂度(O(nlog n))

参考代码:

//problem:ZR247
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=2e5;
int n,a[MAXN+5],pos[MAXN+5];
struct SegmentTree{
	int mn[MAXN*4+5],tag[MAXN*4+5];
	void push_up(int p){
		mn[p]=min(mn[p<<1],mn[p<<1|1]);
	}
	void build(int p,int l,int r){
		if(l==r){
			mn[p]=l-1;
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		push_up(p);
	}
	void upd(int p,int x){
		mn[p]+=x;
		tag[p]+=x;
	}
	void push_down(int p){
		if(tag[p]!=0){
			upd(p<<1,tag[p]);
			upd(p<<1|1,tag[p]);
			tag[p]=0;
		}
	}
	void range_add(int p,int l,int r,int ql,int qr,int x){
		if(ql<=l&&qr>=r){
			upd(p,x);return;
		}
		push_down(p);
		int mid=(l+r)>>1;
		if(ql<=mid)range_add(p<<1,l,mid,ql,qr,x);
		if(qr>mid)range_add(p<<1|1,mid+1,r,ql,qr,x);
		push_up(p);
	}
	SegmentTree(){}
}T;
struct FenwickTree{
	//单点加,后缀求和
	int c[MAXN+5];
	void modify(int p,int x){
		for(;p;p-=(p&(-p)))c[p]+=x;
	}
	int query(int p){
		int res=0;
		for(;p<=n*2;p+=(p&(-p)))res+=c[p];
		return res;
	}
	FenwickTree(){}
}F;
int main() {
	cin>>n;n/=2;
	ll ans=0;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		pos[a[i]]=i;
		ans+=F.query(a[i]);
		F.modify(a[i],1);
	}
	T.build(1,1,n+1);
	for(int i=1;i<=n;++i){
		ans+=T.mn[1];
		T.range_add(1,1,n+1,pos[i*2]+1,n+1,-1);
		T.range_add(1,1,n+1,1,pos[i*2],1);
	}
	cout<<ans<<endl;
	return 0;
}

题解 ZR250 18AB-day2 配对

题目大意

给定一棵(n)个点的边有长度的无根树,小 A 的班里一共有(m)个男生和(m)个女生,他们各自会等概率出现在树上(n)个点中的某一个,(注意同一个点上可能会出现多个人)。

然后小 A 会将他们配对成(m)对男女,设(dis_i)是第(i)对男女在树上的最短距离,小 A 会选择使得(sum_{i=1}^{m}dis_i)最大的配对方案。

现在小 A 想知道,他选择的配对方案中(sum_{i=1}^{m}dis_i)的期望,由于答案可能过大,你只需要输出答案(*n^{2m})后对(10^9+7)取模的值,这个值显然是一个整数。

数据范围:(n,mleq 2500)

本题题解

考虑每条边对答案的贡献。设这条边两侧的节点数分别为(x,y) ((x+y=n)),边权为(w)。考虑它对答案的贡献,我们可以枚举在(x)里有多少男生,多少女生,则贡献为:

[sum_{i=0}^{m}sum_{j=0}^{m}{mchoose i}{mchoose j}x^ix^jy^{m-i}y^{m-j}cdot(min(i,m-j)+min(j,m-i))cdot w ]

如果对所有边暴力计算,总时间复杂度(O(nm^2))。特判(w=0)的情况,可得(70)分。

考虑用前缀和来优化这个式子。具体来说,对每条边,我们先预处理出(f,f_1,f_2)三个数组:

[f[j]={mchoose j}x^{j}y^{m-j}f_1[j]=f[j]cdot jf_2[j]=f[j]cdot(m-j) ]

对这三个数组做前缀和。然后枚举(i),通过分类讨论,把(min)拆开。用预处理好的(f,f_1,f_2)数组,就可以(O(1))计算所有(j)的情况。

时间复杂度(O(nm))

参考代码:

//problem:B
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
const int MAXN=2500;
int fac[MAXN+5],ifac[MAXN+5];
inline int comb(int n,int k){
	if(n<k)return 0;
	return (ll)fac[n]*ifac[k]%MOD*ifac[n-k]%MOD;
}
void facinit(int lim=MAXN){
	fac[0]=1;
	for(int i=1;i<=lim;++i)fac[i]=(ll)fac[i-1]*i%MOD;
	ifac[lim]=pow_mod(fac[lim],MOD-2);
	for(int i=lim-1;i>=0;--i)ifac[i]=(ll)ifac[i+1]*(i+1)%MOD;
}

int n,m;
vector<pii>G[MAXN+5];

int sz[MAXN+5],ans,pw[MAXN+5][MAXN+5],f[MAXN+5],f1[MAXN+5],f2[MAXN+5];
void dfs(int u,int fa){
	sz[u]=1;
	for(int i=0;i<SZ(G[u]);++i){
		int v=G[u][i].fi;
		if(v==fa)continue;
		dfs(v,u);
		if(G[u][i].se){
			for(int y=0;y<=m;++y){
				f[y]=(ll)comb(m,y)*pw[sz[v]][y]%MOD*pw[n-sz[v]][m-y]%MOD;
				f1[y]=(ll)f[y]*y%MOD;
				f2[y]=(ll)f[y]*(m-y)%MOD;
				if(y)add(f[y],f[y-1]),add(f1[y],f1[y-1]),add(f2[y],f2[y-1]);
			}
			for(int x=0;x<=m;++x){
				int fx=(ll)comb(m,x)*pw[sz[v]][x]%MOD*pw[n-sz[v]][m-x]%MOD;
				add(ans,(ll)G[u][i].se*fx%MOD*x%MOD*f[m-x]%MOD);
				add(ans,(ll)G[u][i].se*fx%MOD*mod2(f2[m]-f2[m-x])%MOD);
				add(ans,(ll)G[u][i].se*fx%MOD*(m-x)%MOD*mod2(f[m]-f[m-x])%MOD);
				add(ans,(ll)G[u][i].se*fx%MOD*f1[m-x]%MOD);
			}
		}
		sz[u]+=sz[v];
	}
}

int main() {
	facinit();
	for(int i=1;i<=MAXN;++i){
		pw[i][0]=1;
		for(int j=1;j<=MAXN;++j){
			pw[i][j]=(ll)pw[i][j-1]*i%MOD;
		}
	}
	cin>>n>>m;
	for(int i=1,u,v,w;i<n;++i)cin>>u>>v>>w,G[u].pb(mk(v,w)),G[v].pb(mk(u,w));
	dfs(1,0);
	cout<<ans<<endl;
	return 0;
}

题解 ZR252 18ABday3-亵渎

题目大意

YJC最近在研究炉石。今天他在研究关于其中三张卡牌的一个问题:

这三张卡牌描述如下:

  • 香蕉:使一个单位+1生命。
  • 月火术:使一个单位-1生命。
  • 亵渎:使所有单位-1生命,若有单位死亡,则释放亵渎

现在,你的手牌中有足够多的香蕉月火术以及一张亵渎。你正面临着你对手的一个巨大的场面,你需要清空这个场面。 这个场面上有(n)个随从,每个随从的生命值由输入给出。由于种种原因,你所在的这局游戏这三张卡牌的费用不一定是原来的费用,他们将在输入中被给定。 现在你想知道,为了清空这个场面,至少需要多少费用。

如果你完全没有看懂上面讲了什么,可以看下面的简要题意: 你有一个可重集合(A)(A)中的元素是正整数,当一个元素变为(0)时从集合中删除。 你可以花费(p)使一个元素(+1),花费(q)使一个元素(?1)。 你有一次机会花费(r)使得集合中所有元素(?1),若有元素因此被删除,则重复这个操作(不需要额外的花费)。 你的目标是最小化将集合变为空集的花费。

数据范围:(nleq 5000)(1leq a[i]leq 10^6)(0leq p,q,rleq 10^6)

本题题解

我们最后使用亵渎。

考虑什么情况下,能用一次亵渎就清空整个场面。发现,要求所有剩余随从的生命值(排好序后)为从(1)开始的连续若干个值(可以有重复),例如:$1,1,1,2,2,3,4,4,dots $。

假设,我们一开始就选出了一个子集,作为使用亵渎前,还活着的这些随从。在使用亵渎前,子集里的随从的生命值,一定是上述的那种序列。考虑子集里每个随从,如何与序列里的元素对应。显然,一定让初始生命值最小的那个随从,对应序列里最小的元素;初始生命值第二小的,对应序列里第二小的,......,以此类推。

因此,我们先把所有随从,按初始生命值排序。然后做一个DP。设(dp[i][j])表示考虑了前(i)个随从,使用亵渎前的序列,当前最大元素为(j)时,的最小花费。转移时就考虑是否把当前随从加入使用亵渎的序列即可。

时间复杂度(O(n^2))

参考代码:

#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=5000;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,a[MAXN+5],p,q,r;
ll dp[MAXN+5][MAXN+5];

int main() {
	cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i];
	cin>>p>>q>>r;
	sort(a+1,a+n+1);
	memset(dp,0x3f,sizeof(dp));
	dp[0][0]=0;
	for(int i=1;i<=n;++i){
		for(int j=0;j<i;++j)if(dp[i-1][j]!=INF){
			dp[i][j]=min(dp[i][j],dp[i-1][j]+(ll)a[i]*q);
			ll cost=0;
			if(a[i]<j)cost=(ll)p*(j-a[i]);
			else if(a[i]>j)cost=(ll)q*(a[i]-j);
			dp[i][j]=min(dp[i][j],dp[i-1][j]+cost);
			cost=0;
			if(a[i]<j+1)cost=(ll)p*(j+1-a[i]);
			else if(a[i]>j+1)cost=(ll)q*(a[i]-(j+1));
			dp[i][j+1]=min(dp[i][j+1],dp[i-1][j]+cost);
		}
	}
	ll ans=INF;
	for(int j=0;j<=n;++j)ans=min(ans,dp[n][j]+r*(j>0));
	cout<<ans<<endl;
	return 0;
}

题解 ZR255 18ABday4-世界杯

题目大意

YJC最近在研究世界杯,他拿一半财产压了德国队,另一半财产压了阿根廷队,结果可想而知。YJC表示非常angry,于是又开始研究博彩公司的盈利原理。

假设在世界杯决赛前,有(n)个人参与了赌博。第(i)个人认为法国队赢的概率是(p[i]),克罗地亚队赢的概率是(1?p[i])。对于每一只球队,如果根据博彩公司给出的赔率第(i)个人的期望收益非负,则他会给这只球队下注(a[i])元(设赔率为(x),某人下注了(y)元,如果他赢了可以返还(x?y)元,他输了则会返还(0)元。不论输赢,下注的钱均不返还)。如果两只球队期望收益均非负,则他会给两只球队各下注(a[i])元。

现在博彩公司要决定法国队和克罗地亚队的赔率,使得在自身收益最小的胜负情况下的收益最大。

数据范围:(1leq nleq 10^6), (0leq a[i]leq 100), (0leq p[i]leq 1)。输入、输出精度均为(6)位小数。

本题题解

考虑第(i)个人。设(p_1(i))表示他认为法国队获胜的概率,(p_2(i))表示他认为克罗地亚队获胜的概率,即:(p_1(i)=p[i],p_2(i)=1-p[i])。那么,这个人会下注球队(t) ((tin{1,2})),当且仅当(a[i]cdot p_t(i)cdot xgeq a[i]),其中左边是他期望赢得的金额。把这个不等式化简,得:(xgeq frac{1}{p_t(i)})

(f_t(i)=frac{1}{p_t(i)}),那么,第(i)个人会下注球队(t),当且仅当:(xgeq f_t(i))。所以,对每个球队,有效的赔率取值只有(n+1)(即小于所有(f_t(i)),或者刚好等于(n)个中的某个(f_t(i)))。

暴力的做法是枚举两个球队的赔率分别为多少,再计算收益。时间复杂度(O(n^3))

发现,收益对于两个球队的赔率是可以分别计算的。具体来说,对于球队(t),假设赔率为(x),那么:

  • 如果(t)获胜,博彩公司亏损的金额为:(K_t(x)=sum_{i=1}^{n}[xgeq f_t(i)]cdot (x-1)cdot a[i])
  • 如果(t)失败,博彩公司赚到的金额为:(Z_t(x)=sum_{i=1}^{n}[xgeq f_t(i)]cdot a[i])

求出(K_t)(Z_t)后,我们枚举两个球队分别的赔率:(x_i), (x_j),则盈利为:(minig(Z_1(x_i)-K_2(x_j),Z_2(x_j)-K_1(x_i)ig)),分别代表了球队(1)获胜和球队(2)获胜的情况。暴力求(Z,K),再暴力枚举(x_i,x_j),时间复杂度(O(n^2))

注意到,如果把(f)数组排好序,再从大到小枚举所有(x),则(Z,K)都可以用 two pointers (O(n))求出。

现在,瓶颈在于枚举(x_i,x_j)。后面的(min(dots ))这个式子比较丑。我们把问题写成更简洁的形式:

给定四个数组(A_1[1dots n],A_2[1dots n],B_1[1dots n],B_2[1dots n])。求:(max_{i,jleq n}{min(A_1[i]+B_1[j],A_2[i]+B_2[j])})

这里的((A_1,A_2)),可以看做一个( exttt{pair})型的数组(A),因为(A_1,A_2)出现时下标一样。((B_1,B_2))同理。对(A)数组,按(A_1)从小到大排序。对(B)数组,按(B_1)从小到大排序。(也就是( exttt{pair})默认的比较方式)。

考虑二分答案( ext{mid})。我们要判断是否存在一对(i,j),满足(A_1[i]+B_1[j])(A_2[i]+B_2[j])均大于等于( ext{mid})

枚举(A)的下标(i)。那么,能够满足(A_1[i]+B_1[j]geq ext{mid})(j),一定是(B)序列的一段后缀。我们只需要拿这个后缀里,(B_2[j])最大的(j),看(B_2[j]+A_2[i])是否( ext{mid})即可。这段后缀的长度可以在枚举(i)时用 two pointers 维护。后缀最大的(B_2),可以预处理出来。

时间复杂度(O(nlog n+nlog( ext{eps}^{-1})))

参考代码:

//problem:A
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
typedef double db;

const int MAXN=1e6;
const db INF=1e18;
const db EPS=1e-8;
int n,cntx1,cntx2;
struct People_t{
	db a,p1,p2,needx1,needx2;
}a[MAXN+5];
bool cmp_needx1(People_t a,People_t b){
	return a.needx1<b.needx1;
}
bool cmp_needx2(People_t a,People_t b){
	return a.needx2<b.needx2;
}
db x1[MAXN+5],x2[MAXN+5];
pair<db,db>A[MAXN+5],B[MAXN+5];

int main() {
	//freopen("ex_a2.in","r",stdin);
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>a[i].a>>a[i].p1;
		a[i].p2=(db)1-a[i].p1;
		if(a[i].p1!=0)a[i].needx1=(db)1/a[i].p1,x1[++cntx1]=a[i].needx1;else a[i].needx1=INF;
		if(a[i].p2!=0)a[i].needx2=(db)1/a[i].p2,x2[++cntx2]=a[i].needx2;else a[i].needx2=INF;
	}
	x1[++cntx1]=0;x2[++cntx2]=0;
	
	sort(a+1,a+n+1,cmp_needx1);
	sort(x1+1,x1+cntx1+1);
	for(int i=1,j=0;i<=cntx1;++i){
		A[i].fi=-A[i-1].se*(x1[i]-1);
		A[i].se=A[i-1].se;
		while(j+1<=n && a[j+1].needx1<=x1[i]){
			++j;
			A[i].fi-=(x1[i]-1)*a[j].a;
			A[i].se+=a[j].a;
		}
	}
	
	sort(a+1,a+n+1,cmp_needx2);
	sort(x2+1,x2+cntx2+1);
	for(int i=1,j=0;i<=cntx2;++i){
		B[i].fi=B[i-1].fi;
		B[i].se=-B[i-1].fi*(x2[i]-1);
		while(j+1<=n && a[j+1].needx2<=x2[i]){
			++j;
			B[i].se-=(x2[i]-1)*a[j].a;
			B[i].fi+=a[j].a;
		}
	}
	
	sort(A+1,A+cntx1+1);
	sort(B+1,B+cntx2+1);
	B[cntx2+1].se=-INF;
	for(int i=cntx2;i>=1;--i){
		if(B[i+1].se>B[i].se)B[i].se=B[i+1].se;//后缀max
	}
	double l=0,r=INF;
	while(r-l>EPS){
		double mid=(l+r)/((db)2);
		bool ok=false;
		for(int i=1,j=cntx2+1;i<=cntx1;++i){
			while(j>1 && A[i].fi+B[j-1].fi>=mid)--j;
			if(A[i].se+B[j].se>=mid){
				ok=true;break;
			}
		}
		if(ok)l=mid;
		else r=mid;
	}
	cout<<setiosflags(ios::fixed)<<setprecision(6)<<l<<endl;
	return 0;
}

题解 ZR256 18ABday4-数组

题目大意

YJC最近在研究数组,他认为如果数组的一个区间内不包含重复的元素,那么这个区间是一个优美的区间。

现在YJC弄到了一个interesting的数组,他想知道这个数组有多少个优美的区间。当然,他有时候也会觉得这个数组不够interesting,此时他会修改数组中的一个元素。

YJC发现在修改之后他不会算有多少个优美的区间了,于是他来向你求助。

数据范围:(1leq nleq 10^5), (qleq 2n), (1leq a_ileq n)。((a_i)是数组中的元素)

本题题解

( ext{pre}[i])表示位置(i)上的数,在序列里上一次出现的位置;记( ext{nxt}[i])表示位置(i)上的数,在序列里下一次出现的位置。像本题这种限制数在区间里出现次数的问题,往往可以转化为( ext{pre}[i])( ext{nxt}[i])之间的关系。这是一个小套路。

考虑枚举区间的右端点(i),看最靠前的、合法的左端点在哪里,记这个位置为(f_i)。那么,左端点在([f_i,i])之间都是合法的区间。故答案为(sum_{i=1}^{n}(i-f_i+1))。容易发现,以(i)为右端点的(f_i),一定至少大于等于(f_{i-1})。同时,为了保证(i)这个数在区间里只出现一次,(f_i)又需要大于( ext{pre}[i])。所以,(f_{i}=max(f_{i-1}, ext{pre}[i]+1))。有了这个递推式,再看求答案的式子,发现答案就等于(sum_{i=1}^{n}(i-max_{j=1}^{j} ext{pre}[j])),于是问题转化为维护( ext{pre})数组的“前缀(max)”之和。

一次修改操作,只会对不超过(3)个点的( ext{pre})值产生影响,分别是:(x),原来的( ext{nxt}[x]),新的( ext{nxt}[x])这三个位置。这几个位置是很容易求出的。我们可以开(n)( exttt{set}),记录每个值在哪些位置出现过。然后在( exttt{set})里二分,就能找到这几个要修改的位置。

于是问题转化为,维护一个序列,支持单点修改,询问“前缀最大值”之和

考虑用线段树维护。对线段树上,每个节点(p=[l,r]),我们记录两个值:

  • ( ext{mx}[p]),表示区间([l,r])( ext{pre})数组的最大值。
  • ( ext{sum}[p]),表示区间([l,r])里,每个位置(i),从(l)(i)的“前缀最大值”之和。

难点在于如何( ext{pushup}),也就是合并左、右两个儿子。合并时,( ext{mx})直接取两个儿子的较大值即可。左儿子的( ext{sum}),可以直接贡献到当前节点上。而右儿子的里,需要先对左儿子的最大值( ext{mx}[ ext{left_son}])(max),再累加到( ext{sum}[p])中。怎么求线段树里某个区间对某个值取(max)后的和呢?我们可以递归下去,在(O(log n))的时间里求出。具体代码如下:

ll cmax_sum(int p,int l,int r,int x){
	if(l==r)return max(x,mx[p]);
	int mid=(l+r)>>1;
	if(mx[p<<1]<=x)return (ll)x*(mid-l+1)+cmax_sum(p<<1|1,mid+1,r,x);
	else return cmax_sum(p<<1,l,mid,x)+sum[p]-sum[p<<1];
}
void push_up(int p){
	int ls=p<<1,rs=p<<1|1;
	mx[p]=max(mx[ls],mx[rs]);
	sum[p]=sum[ls]+cmax_sum(rs,tl[rs],tr[rs],mx[ls]);
}

时间复杂度(O(nlog^2n))

参考代码:

//problem:ZR256
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=1e5;
int n,a[MAXN+5],pos[MAXN+5],pre[MAXN+5],nxt[MAXN+5];
set<int>s[MAXN+5];

struct SegmentTree{
	ll sum[MAXN*4+5];
	int mx[MAXN*4+5];
	int tl[MAXN*4+5],tr[MAXN*4+5];
	ll cmax_sum(int p,int l,int r,int x){
		if(l==r)return max(x,mx[p]);
		int mid=(l+r)>>1;
		if(mx[p<<1]<=x)return (ll)x*(mid-l+1)+cmax_sum(p<<1|1,mid+1,r,x);
		else return cmax_sum(p<<1,l,mid,x)+sum[p]-sum[p<<1];
	}
	void push_up(int p){
		int ls=p<<1,rs=p<<1|1;
		mx[p]=max(mx[ls],mx[rs]);
		sum[p]=sum[ls]+cmax_sum(rs,tl[rs],tr[rs],mx[ls]);
	}
	void build(int p,int l,int r){
		tl[p]=l;tr[p]=r;
		if(l==r){
			sum[p]=mx[p]=pre[l];
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid);
		build(p<<1|1,mid+1,r);
		push_up(p);
	}
	void modify(int p,int l,int r,int pos,int x){
		if(l==r){
			sum[p]=mx[p]=x;
			return;
		}
		int mid=(l+r)>>1;
		if(pos<=mid)modify(p<<1,l,mid,pos,x);
		else modify(p<<1|1,mid+1,r,pos,x);
		push_up(p);
	}
	SegmentTree(){}
}T;

int main() {
	cin>>n;
	for(int i=1;i<=n;++i)cin>>a[i],s[a[i]].insert(i);
	for(int i=1;i<=n;++i)pre[i]=pos[a[i]],pos[a[i]]=i;
	for(int i=1;i<=n;++i)pos[i]=n+1;
	for(int i=n;i>=1;--i)nxt[i]=pos[a[i]],pos[a[i]]=i;
	T.build(1,1,n);
	ll sum=(ll)n*(n+1)/2;
	int q;cin>>q;while(q--){
		int op;cin>>op;
		if(!op){
			cout<<sum-T.sum[1]<<endl;
		}
		else{
			int p,v;cin>>p>>v;
			if(v==a[p])continue;
			if(nxt[p]!=n+1){
				pre[nxt[p]]=pre[p];
				T.modify(1,1,n,nxt[p],pre[nxt[p]]);
			}
			if(pre[p]!=0)nxt[pre[p]]=nxt[p];
			s[a[p]].erase(p);
			set<int>::iterator it=s[v].lob(p);
			if(it!=s[v].end()){
				nxt[p]=*it;
				pre[nxt[p]]=p;
				T.modify(1,1,n,nxt[p],pre[nxt[p]]);
			}
			else nxt[p]=n+1;
			if(it!=s[v].begin()){
				--it;
				pre[p]=*it;
				nxt[pre[p]]=p;
			}
			else pre[p]=0;
			T.modify(1,1,n,p,pre[p]);
			s[v].insert(p);
			a[p]=v;
			//cout<<"pre ";for(int i=1;i<=n;++i)cout<<pre[i]<<" ";cout<<endl;
		}
	}
	return 0;
}

题解 ZR258 18ABday5-友谊巨轮

题面下载

本题题解

先不管问题。我们尝试维护出每个时刻每个人的巨轮是谁。直观的做法是用一个二维数组,存pair<long long,int>类型,分别表示两人之间的消息数量和最近一次发消息的时间。我们需要支持单点修改,并维护这个数组每一行的最大值。直观的做法是每行开一个线段树,但这样空间会超限。改成动态开点线段树就好了:因为修改次数是(O(n))的,每次修改只会在线段树上新增(O(log n))个节点,所以空间复杂度是(O(nlog n)),可以接受。

维护出每个人的巨轮后,考虑回答询问。发现每个时刻,只有不超过(4)个人的巨轮会变化(新加入的一对人,和刚刚过期的一对人)。我们说一个人的“状态”是指,他是否是单向巨轮。那么,如果(x)这个人的巨轮变化了,那么我们只需要更新:(1) 原来(x)的巨轮;(2) (x);(3) (x)的新巨轮;这三个点的状态。容易发现,其他点的状态不会改变。所以可以(O(1))完成所有更新。

时间复杂度(O(nlog n))

参考代码:

//problem:A
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=1e5;
int n,t,m;
map<pii,pair<ll,int> >mp;
struct Event_t{
	int a,b,c;
	Event_t(){}
	Event_t(int _a,int _b,int _c){
		a=_a;
		b=_b;
		c=_c;
	}
}q[MAXN+5];

const int SIZE=2e7;
int rt[MAXN+5],cnt,mxpos[SIZE],ls[SIZE],rs[SIZE];
pair<ll,int> mx[SIZE];
int newnode(){
	++cnt;
	ls[cnt]=rs[cnt]=mxpos[cnt]=0;
	mx[cnt]=mk(0,0);
	return cnt;
}
void push_up(int p){
	if(!ls[p] && !rs[p])return;
	if(!ls[p]){
		mx[p]=mx[rs[p]];
		mxpos[p]=mxpos[rs[p]];
		return;
	}
	if(!rs[p]){
		mx[p]=mx[ls[p]];
		mxpos[p]=mxpos[ls[p]];
		return;
	}
	if(mx[ls[p]]>mx[rs[p]]){
		mx[p]=mx[ls[p]];
		mxpos[p]=mxpos[ls[p]];
	}
	else{
		mx[p]=mx[rs[p]];
		mxpos[p]=mxpos[rs[p]];
	}
}
void modify(int& p,int l,int r,int pos,const pair<ll,int>& x){
	int tmp=p;
	p=newnode();
	if(tmp){
		ls[p]=ls[tmp];
		rs[p]=rs[tmp];
	}
	if(l==r){
		mx[p]=x;
		mxpos[p]=l;
		return;
	}
	int mid=(l+r)>>1;
	if(pos<=mid)
		modify(ls[p],l,mid,pos,x);
	else
		modify(rs[p],mid+1,r,pos,x);
	push_up(p);
}
int ans;
bool in_ans[MAXN+5];
void update_ans(int a){
	if(in_ans[a])ans--;
	in_ans[a]=0;
	if(rt[a] && mx[rt[a]].fi!=0){
		int f=mxpos[rt[a]];
		if(mxpos[rt[f]]!=a){
			in_ans[a]=1;
			ans++;
		}
	}
}
vector<int>v;
void tree_modify(int a,int b){
	if(rt[a] && mx[rt[a]].fi!=0){
		v.pb(mxpos[rt[a]]);
	}
	if(rt[b] && mx[rt[b]].fi!=0){
		v.pb(mxpos[rt[b]]);
	}
	modify(rt[a],1,n,b,mp[mk(a,b)]);
	modify(rt[b],1,n,a,mp[mk(a,b)]);
	if(rt[a] && mx[rt[a]].fi!=0){
		v.pb(mxpos[rt[a]]);
	}
	if(rt[b] && mx[rt[b]].fi!=0){
		v.pb(mxpos[rt[b]]);
	}
}
void solve(){
	cnt=0;ans=0;
	mp.clear();
	for(int i=1;i<=n;++i){
		rt[i]=0;
		in_ans[i]=0;
	}
	for(int i=1;i<=t;++i){
		cin>>q[i].a>>q[i].b>>q[i].c;
		if(q[i].a>q[i].b)swap(q[i].a,q[i].b);
		
		mp[mk(q[i].a,q[i].b)].fi+=q[i].c;
		mp[mk(q[i].a,q[i].b)].se=i;
		tree_modify(q[i].a,q[i].b);
		
		if(i-m>=1){
			mp[mk(q[i-m].a,q[i-m].b)].fi-=q[i-m].c;
			if(mp[mk(q[i-m].a,q[i-m].b)].se==i-m){
				mp[mk(q[i-m].a,q[i-m].b)].se=0;
			}
			tree_modify(q[i-m].a,q[i-m].b);
			update_ans(q[i-m].a);
			update_ans(q[i-m].b);
		}
		//for(int j=1;j<=n;++j)update_ans(j);
		for(int i=0;i<SZ(v);++i){
			update_ans(v[i]);
		}
		v.clear();
		update_ans(q[i].a);
		update_ans(q[i].b);
		cout<<ans<<endl;
	}
}
int main() {
	int Tes;cin>>Tes;while(Tes--){
		cin>>n>>t>>m;
		solve();
	}
	return 0;
}

题解 ZR260 18ABday5-构解巨树

题面下载

本题题解

首先,给定一棵树,如何求出所有点对距离和?朴素做法是枚举两个点,求一遍距离,时间复杂度(O(n^3))。如果求距离用(O(1))LCA的方法求,则优化到(O(n^2))。换个角度,累加“每条边的贡献”,例如,考虑(u)(fa(u))之间的边,则答案为(sum_{u eq 1} ext{size}_ucdot (n- ext{size}_u)),于是只需要求出每个点的( ext{size})即可,时间复杂度(O(n))。本题就是在这种考虑“每条边贡献”的思想的基础上完成的。

回到本题,也就是这个“树套树”的模型中。我们把贡献分为三类:

  1. 小树和小树之间的边。也就是(m)条边之一。这些边两侧各有若干棵完整的小树。设它子树里有(x)棵小树,则对答案的贡献就是(xcdot ncdot (mcdot n-xcdot n))。于是直接对大树dfs,就可以(O(m))求出这类边的贡献。
  2. 小树内部的边,同一小树内点对经过这条边的次数。这样的贡献,对每棵小树的同一条边来说是相同的。所以只需要对小树做一遍dfs,求出贡献和后乘以(m)就行。时间复杂度(O(n))
  3. 小树内部的边,来自不同小树点对经过这条边的次数。

第3类贡献比较麻烦。我们来仔细讨论。

依次考虑每棵小树。对当前小树,先以(1)为根,把它看成一棵有根树。然后转化为,有一棵树,然后给定一些二元组((x,y)),表示在节点(x)上,挂了(ycdot n)个点。

我们记小树节点(u)子树内的点数为( ext{in}(u)),子树外的点数为( ext{out}(u))。初始时,只考虑了当前小树里的点(没有考虑二元组挂上去的点),也就是( ext{in}(u)= ext{size}_u, ext{out}(u)=n- ext{size}_u)。请注意,这里(u eq 1),因为(1)节点和父亲之间没有边,自然也不会产生贡献。然后我们依次考虑每个二元组((x,y)),把它挂上去时,会新增的贡献数是:

  • 对于(x)的所有祖先(u)(不含(1)),答案增加(ycdot ncdot ext{out}(u))
  • 对于其他节点(v)(不含(1)),答案增加(ycdot ncdot ext{in}(v))

然后用二元组((x,y))来更新每个节点的( ext{in}, ext{out})

  • 对于(x)的所有祖先(u)(不含(1)),( ext{in}(u))增加(ycdot n)
  • 对于其他节点(v)(不含(1)),( ext{out}(v))增加(ycdot n)

可以看出,我们要支持,(1) 给所有祖先加,(2) 给所有其他节点加。而“给其他节点加”,可以转化为先给所有节点加,然后再给祖先减掉。所以只需要支持对所有祖先加,对所有祖先求和即可。可以用树链剖分+线段树实现。

虽然每个小树上可能挂多个二元组,但二元组的本质就是大树上的边,所以总数只有(O(m))个。每次树链剖分+线段树操作的复杂度是(O(log^2n))的,所以总时间复杂度(O(mlog^2n))

有一个小问题是,每考虑完一个小树后,要清空线段树。准确来说不是清空,是还原成( ext{in}(u)= ext{size}_u, ext{out}(u)=n- ext{size}_u)的情况。一种做法是把所有操作反着做一遍:即区间加变成区间减。但是这样常数一下子翻倍。有一种更巧妙的方法是维护时间戳。在线段树上走到一个节点时,如果它的时间戳和当前时间不一样,说明它还停留在之前的某个阶段,我们直接把它还原,然后把它的时间戳改成当前时间即可。

参考代码:

//problem:ZR260
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MOD=1e9+7;
const int MAXN=1e5;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}

int n,m,ans;

namespace SmallTree{
struct EDGE{int nxt,to;}edge[MAXN*2+5];
int head[MAXN+5],tot;
inline void add_edge(int u,int v){edge[++tot].nxt=head[u],edge[tot].to=v,head[u]=tot;}

int fa[MAXN+5],sz[MAXN+5],son[MAXN+5],dep[MAXN+5];
void dfs1(int u){
	sz[u]=1;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa[u])continue;
		fa[v]=u;
		dep[v]=dep[u]+1;
		dfs1(v);
		sz[u]+=sz[v];
		if(!son[u]||sz[v]>sz[son[u]])son[u]=v;
	}
}
int top[MAXN+5],dfn[MAXN+5],ofn[MAXN+5],rev[MAXN+5],cnt_dfn;
void dfs2(int u,int t){
	top[u]=t;
	dfn[u]=++cnt_dfn;
	rev[cnt_dfn]=u;
	if(son[u])dfs2(son[u],t);
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v);
	}
	ofn[u]=cnt_dfn;
}
}//namespace SmallTree

struct SegmentTree{
	int curtim;
	int sum[MAXN*4+5],tag[MAXN*4+5],initsum[MAXN*4+5],tim[MAXN*4+5];
	void push_up(int p){
		sum[p]=mod1(sum[p<<1]+sum[p<<1|1]);
	}
	void build(int p,int l,int r,bool flag){
		// flag:
		// in 0; out 1
		if(l==r){
			using SmallTree::sz;
			using SmallTree::rev;
			if(!flag)
				initsum[p]=sum[p]=sz[rev[l]];
			else
				initsum[p]=sum[p]=n-sz[rev[l]];
			return;
		}
		int mid=(l+r)>>1;
		build(p<<1,l,mid,flag);
		build(p<<1|1,mid+1,r,flag);
		push_up(p);
		initsum[p]=mod1(initsum[p<<1]+initsum[p<<1|1]);
	}
	void clr(int p){
		if(tim[p]!=curtim){
			tim[p]=curtim;
			sum[p]=initsum[p];
			tag[p]=0;
		}
	}
	void upd(int p,int l,int r,int x){
		add(sum[p],(ll)x*(r-l+1)%MOD);
		add(tag[p],x);
	}
	void push_down(int p,int l,int mid,int r){
		clr(p<<1);
		clr(p<<1|1);
		if(tag[p]){
			upd(p<<1,l,mid,tag[p]);
			upd(p<<1|1,mid+1,r,tag[p]);
			tag[p]=0;
		}
	}
	void range_add(int p,int l,int r,int ql,int qr,int x){
		clr(p);
		if(ql<=l && qr>=r){
			upd(p,l,r,x);
			return;
		}
		int mid=(l+r)>>1;
		push_down(p,l,mid,r);
		if(ql<=mid)
			range_add(p<<1,l,mid,ql,qr,x);
		if(qr>mid)
			range_add(p<<1|1,mid+1,r,ql,qr,x);
		push_up(p);
	}
	int query(int p,int l,int r,int ql,int qr){
		clr(p);
		if(ql<=l && qr>=r){
			return sum[p];
		}
		int mid=(l+r)>>1;
		push_down(p,l,mid,r);
		int res=0;
		if(ql<=mid)
			res=query(p<<1,l,mid,ql,qr);
		if(qr>mid)
			add(res,query(p<<1|1,mid+1,r,ql,qr));
		push_up(p);
		return res;
	}
}T_in,T_out;

int jump(int _u,int x){
	using namespace SmallTree;
	int u=_u;
	int res=T_in.query(1,1,n,2,n);
	while(top[u]!=1){
		add(res,T_out.query(1,1,n,dfn[top[u]],dfn[u]));
		sub(res,T_in.query(1,1,n,dfn[top[u]],dfn[u]));
		u=fa[top[u]];
	}
	if(u!=1){
		add(res,T_out.query(1,1,n,dfn[1]+1,dfn[u]));
		sub(res,T_in.query(1,1,n,dfn[1]+1,dfn[u]));
	}
	res=(ll)res*x%MOD;
	
	u=_u;
	T_out.range_add(1,1,n,2,n,x);
	while(top[u]!=1){
		T_in.range_add(1,1,n,dfn[top[u]],dfn[u],x);
		T_out.range_add(1,1,n,dfn[top[u]],dfn[u],mod2(-x));
		u=fa[top[u]];
	}
	if(u!=1){
		T_in.range_add(1,1,n,dfn[1]+1,dfn[u],x);
		T_out.range_add(1,1,n,dfn[1]+1,dfn[u],mod2(-x));
	}
	
	return res;
}

namespace BigTree{
struct EDGE{int nxt,to,from_node,to_node;}edge[MAXN*2+5];
int head[MAXN+5],tot;
inline void add_edge(int u,int v,int a,int b){
	edge[++tot].nxt=head[u],edge[tot].to=v,head[u]=tot;
	edge[tot].from_node=a;
	edge[tot].to_node=b;
}

int sz[MAXN+5];
void dfs_solve(int u,int fa,int connect_node){
	vector<pii>vec;
	sz[u]=1;
	for(int i=head[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa)continue;
		dfs_solve(v,u,edge[i].to_node);
		vec.pb(mk(edge[i].from_node,sz[v]));
		add(ans,(ll)sz[v]*n%MOD*mod2((ll)m*n%MOD-(ll)sz[v]*n%MOD)%MOD);
		sz[u]+=sz[v];
	}
	if(fa!=0){
		vec.pb(mk(connect_node,m-sz[u]));
	}
	T_in.curtim=u;
	T_out.curtim=u;//线段树清空
	for(int i=0;i<SZ(vec);++i){
		add(ans,jump(vec[i].fi,(ll)vec[i].se*n%MOD));
	}
}
}//namespace BigTree

int main() {
	cin>>n>>m;
	for(int i=1;i<=n-1;++i){
		int u,v;cin>>u>>v;
		SmallTree::add_edge(u,v);
	}
	for(int i=1;i<=m-1;++i){
		int u,v,a,b;cin>>u>>v>>a>>b;
		BigTree::add_edge(u,v,a,b);
	}
	SmallTree::dfs1(1);
	SmallTree::dfs2(1,1);
	for(int i=2;i<=n;++i){
		using SmallTree::sz;
		add(ans,(ll)sz[i]*(n-sz[i])%MOD*m%MOD);
	}
	
	T_in.build(1,1,n,0);
	T_out.build(1,1,n,1);
	BigTree::dfs_solve(1,0,0);
	
	cout<<ans<<endl;
	return 0;
}

题解 ZR261 18ABday6-萌新拆塔

题面下载

本题题解

这种类似于全排列的搜索,往往都指向一个优化:状压DP

(dp[mask])表示已经打败了(mask)里这些怪物,剩下的血量的最大值。除血量外,攻击、防御、魔防值都是确定的,可以顺便维护出来。

但是这样会遇到一个问题:什么时候取宝石。我们发现,一打完立即取走宝石并不一定是最优的,因为有“模仿怪”这种东西存在。所以这个状态要改一改。设(dp[s_1][s_2])表示打败了(s_1)里面这些怪物,取走了(s_2)里这些宝石,剩余血量的最大值。显然,(s_2)一定是(s_1)的子集,所以总状态数是(3^n)。在具体实现时,我们不开二维数组,而是用三进制状态来表示:未被打败、被打败了但没有取走宝石、取走了宝石,这三种状态。

时间复杂度(O(3^ncdot n))

参考代码:

//problem:ZR261
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=14;
const int MAX_MASK=5e6;

int pw3[MAXN+5];
void init(){
	pw3[0]=1;
	for(int i=1;i<=MAXN;++i)pw3[i]=pw3[i-1]*3;
}
struct Ternary_t{
	int x;
	Ternary_t(int _x=0){
		x=_x;
	}
	Ternary_t operator>>(const int& rhs){
		Ternary_t res=*this;
		res.x/=pw3[rhs];
		return res;
	}
	Ternary_t& operator>>=(const int& rhs){
		*this=((*this)>>rhs);
		return *this;
	}
	Ternary_t& operator++(){
		x++;
		return *this;
	}
	int binary(){
		int res=0;
		for(int i=0;i<=13;++i)if(((*this)>>i)%3!=0)res^=(1<<i);
		return res;
	}
	operator int()const{return x;}
};

int n,K;
int h,a,d,m;
struct Monster_t{
	int H,A,D,S,a,d,m,h;
	void input(){
		cin>>H>>A>>D>>S>>a>>d>>m>>h;
	}
}mons[MAXN+5];
vector<int>G[MAXN+5];
bool good_mask[1<<MAXN];

struct State_t{
	ll h,a,d,m;
	State_t(){}
	State_t(ll _h,ll _a,ll _d,ll _m){
		h=_h;a=_a;d=_d;m=_m;
	}
	bool operator<(const State_t& rhs){
		return h<rhs.h;
	}
}dp[MAX_MASK];

const State_t FAIL_STATE=State_t(-1,0,0,0);

State_t fight(const State_t& i,int j){
	bool monster_first=(mons[j].S&1);
	bool ignore_my_def=((mons[j].S>>1)&1);
	bool two_att=((mons[j].S>>2)&1);
	bool same_with_me=((mons[j].S>>3)&1);
	
	ll A=mons[j].A;
	ll D=mons[j].D;
	if(same_with_me){A=i.a;D=i.d;}
	
	int you_to_i=A-(ignore_my_def?0:i.d);
	int i_to_you=i.a-D;
	if(i_to_you<=0)return FAIL_STATE;
	
	ll rounds=mons[j].H/i_to_you+(mons[j].H%i_to_you!=0);
	ll you_att=max(0LL,(rounds-(!monster_first))*(two_att?2:1)*you_to_i-i.m);
	
	if(you_att>=i.h)return FAIL_STATE;
	
	State_t res=i;
	res.h-=you_att;
	return res;
}
State_t pick(const State_t& i,int j){
	State_t res=i;
	res.h+=mons[j].h;
	res.a+=mons[j].a;
	res.d+=mons[j].d;
	res.m+=mons[j].m;
	return res;
}

int main() {
	init();
	int T;cin>>T;while(T--){
		cin>>h>>a>>d>>m;
		cin>>n;
		for(int i=1;i<=n;++i){
			mons[i].input();
			vector<int>().swap(G[i]);
		}
		cin>>K;
		for(int i=1;i<=K;++i){
			int u,v;cin>>u>>v;
			G[v].pb(u);
		}
		for(int i=0;i<(1<<n);++i){
			good_mask[i]=true;
			for(int j=1;j<=n;++j)if((i>>(j-1))&1){
				for(int k=0;k<SZ(G[j]);++k){
					if(!((i>>(G[j][k]-1))&1)){
						good_mask[i]=false;
						break;
					}
				}
				if(!good_mask[i])break;
			}
		}
		dp[0]=State_t(h,a,d,m);
		for(Ternary_t mask=1;mask<=pw3[n]-1;++mask){
			dp[mask]=FAIL_STATE;
		}
		for(Ternary_t mask=0;mask<=pw3[n]-2;++mask){
			if(dp[mask].h==-1)continue;
			int bi=mask.binary();
			for(int j=1;j<=n;++j){
				if((mask>>(j-1))%3==0){
					//新打一个怪
					if(good_mask[bi|(1<<(j-1))]){
						State_t tmp=fight(dp[mask],j);
						if(dp[mask+pw3[j-1]]<tmp){
							dp[mask+pw3[j-1]]=tmp;
						}
					}
				}
				else if((mask>>(j-1))%3==1){
					//捡个宝石
					State_t tmp=pick(dp[mask],j);
					if(dp[mask+pw3[j-1]]<tmp){
						dp[mask+pw3[j-1]]=tmp;
					}
				}
			}
		}
		cout<<dp[pw3[n]-1].h<<endl;
	}
	return 0;
}

题解 ZR263 18ABday6-风花雪月

题面下载

本题题解

(s=sum c_i)

首先,抽卡的总次数是有限的。当总卡数达到(4s+3n)时,无论抽到的是什么卡,都必能兑换所有服装。这是因为,每(4)张同一种类的卡,就能兑换任意一种服装。而为了避免卡被模(4)的余数浪费掉,所以预先安排(3n)张卡,专门用于被余数浪费。如果你不能理解(+3n),可以想一想(c={0,0,1}),抽到的卡为({2,2,0})的情况。

现在,考虑手上抽到的卡,把每种卡被抽到的数量的序列,称为一个“状态”。那么,如果当前手上抽到的卡,已经能够获得(n)件衣服,我们称这个状态为“不好”的。否则,也就是无法获得(n)件衣服时,我们称这样的状态是“好的”。

题目问,第一次达到“不好的”状态,所经过的期望操作次数。就相当于问,经过的“好的”状态的数量的期望。另外,显然,“好的”状态之间,是一个严格的树形结构,不会出现环(因为每个状态比它的父亲多一张卡)。所以,每个状态最多只会被经过(1)次。因此,根据期望的线性性,经过的“好的”状态的数量的期望,就等于经过每个“好的”状态的概率之和(其实本质是概率( imes 1)之和,因为最多只会经过(1)次)。

可以用DP求所有“好的”状态的概率之和。依次考虑每种卡。假设当前考虑了前(i)种卡。我们要知道前(i)种卡,能为后面贡献多少张卡,记为(k)。这里的“贡献”,就是指每四张相同类型的卡换任意一张其他卡。例如,如果前(i)种卡里,某一种卡需要(c)张,而实际抽到了(t)张,则会令贡献值(k)增加(lfloorfrac{t-c}{4} floor)。当然,如果前(i)种卡,自己都没有抽满,需要后面的卡来贡献它们,则(k)为负数。

于是可以设计一个DP状态:设(dp[i][j][k])表示考虑了前(i)种卡,总共选出了(j)张卡,贡献值为(k)的方案数。转移时,枚举第(i)种卡选了(t)张。如果(t<c_i),则新的(k‘=k-(c_i-t));否则(k‘=k+lfloorfrac{t-c_i}{4} floor)

在转移时,根据新增的卡数(t),我们乘上系数(frac{(p_i)^t}{t!})。在最后统计答案时,如果选出的总卡数为(J),我们再乘上(J!)。最后就相当于乘以了(frac{J!}{prod(t!)}),这也就是含重复元素的排列计数。

分析时间复杂度。(ileq n,0leq jleq 4s+3n,-sleq kleq s)。转移时,还要枚举一个和(j)同阶的(t)。总时间复杂度(O(n(4s)^2s))卡卡就过了

参考代码:

//problem:ZR263
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

const int MAXN=10,MAXM=1600,BASE=401;
const int MOD=1e9+7;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
int fac[MAXM+5],ifac[MAXM+5];
inline int comb(int n,int k){
	if(n<k)return 0;
	return (ll)fac[n]*ifac[k]%MOD*ifac[n-k]%MOD;
}
void facinit(int lim){
	fac[0]=1;
	for(int i=1;i<=lim;++i)fac[i]=(ll)fac[i-1]*i%MOD;
	ifac[lim]=pow_mod(fac[lim],MOD-2);
	for(int i=lim-1;i>=0;--i)ifac[i]=(ll)ifac[i+1]*(i+1)%MOD;
}

int n,p[MAXN+5],c[MAXN+5],suf_c[MAXN+5];
int dp[MAXN+5][MAXM+MAXN*3+5][BASE*2+5];

int main() {
	facinit(MAXM);
	cin>>n;
	int sum=0;
	for(int i=1;i<=n;++i){
		cin>>p[i];
		add(sum,p[i]);
	}
	int isum=pow_mod(sum,MOD-2);
	for(int i=1;i<=n;++i){
		p[i]=(ll)p[i]*isum%MOD;
	}
	sum=0;
	for(int i=1;i<=n;++i){
		cin>>c[i];
		sum+=c[i];
	}
	for(int i=n;i>=1;--i){
		suf_c[i]=suf_c[i+1]+c[i];
	}
	int m=sum*4+n*3;
	dp[0][0][0+BASE]=1;
	for(int i=1;i<=n;++i){
		static int f[MAXM+5];
		int w=1;
		for(int j=0;j<=m;++j){
			f[j]=(ll)w*ifac[j]%MOD;
			w=(ll)w*p[i]%MOD;
		}
		for(int j=0;j<=m;++j){
			for(int k=-sum,v;k<=suf_c[i];++k)if(v=dp[i-1][j][k+BASE]){
				for(int t=0;t+j<=m;++t){
					int newk=k;
					if(t<c[i])newk-=c[i]-t;
					else newk+=(t-c[i])/4;
					if(newk<-sum)continue;
					if(newk>suf_c[i+1])break;
					add(dp[i][t+j][newk+BASE],(ll)v*f[t]%MOD);
				}
			}
		}
	}
	int ans=0;
	for(int j=0;j<=m;++j){
		int tmp=0;
		for(int k=-sum;k<0;++k){
			add(tmp,dp[n][j][k+BASE]);
		}
		add(ans,(ll)tmp*fac[j]%MOD);
	}
	cout<<ans<<endl;
	return 0;
}

以上是关于2023年团体程序设计天梯赛 题解的主要内容,如果未能解决你的问题,请参考以下文章

2019年GPLT L2-4 彩虹瓶 比赛题解 中国高校计算机大赛-团体程序设计天梯赛题解

2019年GPLT L2-3 深入虎穴 比赛题解 中国高校计算机大赛-团体程序设计天梯赛题解

2019年GPLT L2-1 特立独行的幸福 比赛题解 中国高校计算机大赛-团体程序设计天梯赛题解

题解PTA团体程序设计天梯赛L1-042 日期格式化 (5 分) Go语言|Golang

2023GPLT团体程序设计天梯赛 记录

题解PTA团体程序设计天梯赛L1-033 出生年 (15 分) Go语言|Golang