2023年团体程序设计天梯赛 题解
Posted HEY
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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)里有多少男生,多少女生,则贡献为:
如果对所有边暴力计算,总时间复杂度(O(nm^2))。特判(w=0)的情况,可得(70)分。
考虑用前缀和来优化这个式子。具体来说,对每条边,我们先预处理出(f,f_1,f_2)三个数组:
对这三个数组做前缀和。然后枚举(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))。本题就是在这种考虑“每条边贡献”的思想的基础上完成的。
回到本题,也就是这个“树套树”的模型中。我们把贡献分为三类:
- 小树和小树之间的边。也就是(m)条边之一。这些边两侧各有若干棵完整的小树。设它子树里有(x)棵小树,则对答案的贡献就是(xcdot ncdot (mcdot n-xcdot n))。于是直接对大树dfs,就可以(O(m))求出这类边的贡献。
- 小树内部的边,同一小树内的点对经过这条边的次数。这样的贡献,对每棵小树的同一条边来说是相同的。所以只需要对小树做一遍dfs,求出贡献和后乘以(m)就行。时间复杂度(O(n))。
- 小树内部的边,来自不同小树的点对经过这条边的次数。
第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 特立独行的幸福 比赛题解 中国高校计算机大赛-团体程序设计天梯赛题解