ICPC2020沈阳区域赛题解 CDEFGHIKM
The 2020 ICPC Asia Shenyang Regional Contest
ABCDEFGHIJKLM
由于最近很忙,题解写得不是很详尽,如果你认为有的题看得不是很明白,可以留言哦。
G. The Witchwood
求 \(n\) 个元素中最大的 \(k\) 个元素之和
排序然后求即可。
int n, k;
int a[N];
int main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i ++)
scanf("%d", &a[i]);
std::sort(a + 1, a + n + 1);
ll ans = 0;
for (int i = n; i >= n - k + 1; i --)
ans += std::max(0, a[i]);
printf("%lld", ans);
return 0;
}
F. Kobolds and Catacombs
给一个长为 \(n\) 的序列 \(a\),你可以将 \(a\) 划分成若个不重叠的子段,对每个子段升序排序,问最多能划分多少个子段,使得每个子段单独排序后整个序列也是升序的。
考虑从左到右划分(反向是一样的),如果当前划分的子段中的所有数不大于子段右侧的某个数,那个这个子段是可以划分的。然后按这种方法划分的子段尽可能短即可。具体实现见代码吧。
int n, a[MAXN], val[MAXN];
std::map<int, int> L;
int main() {
// freopen("r.in", "r", stdin);
scanf("%d", &n);
for(int i=1; i<=n; ++i) {
scanf("%d", &a[i]);
val[i] = a[i];
}
std::sort(val + 1, val + n + 1);
for (int i = n; i >= 1; i --)
L[val[i]] = i;
int l=1, r=1, ans=0, R = L[a[1]] ++;
while(true) {
// printf("?? %d %d\n", l, r);
while(R != r) {
++ r;
R = std::max(R, L[a[r]] ++);
}
++ ans;
l = r+1, r = l;
if(l>n) break;
R = L[a[r]] ++;
}
printf("%d", ans);
return 0;
}
D. Journey to Un'Goro
请构造长为 \(n\) 的 \(01\) 序列,使得满足 \(i\) 到 \(j\) 中 \(1\) 的数量是奇数的数对 \((i,j)\;(1\leq i\leq j\leq n)\) 总数尽可能多,如果有多个序列能取到最大值,按字典序大小输出前 \(100\) 个。
\(1\leq n\leq 10^5\)
首先有一个结论:
- 对 \(01\) 序列作前缀异或和,并令前缀和第 \(0\) 位的 \(0\) 也算在内,共有 \((n+1)\) 个 \(01\) 数:如果 \(n+1\) 为偶数,则前缀和 \(0\) 的个数等于 \(1\) 的个数;如果 \(n+1\) 为奇数,则 \(0\) 的个数与 \(1\) 的个数差 \(1\)。因为一对配对的前缀和中 \(01\) 的数,对应原序列中一个 \(1\) 数量时奇数的子段,而 \(01\) 个数相当时对数最多。
那么我们的问题则是对一个(或两个,这时 \(n\) 为偶数) \(0\) 和 \(1\) 个数都确定的前缀和序列进行排列(需保证第一位是 \(0\)),并找出其中对应原序列中字典序最小的若干个。
显然前缀和序列中第一个 \(1\) 出现的位置越靠后越好。
当 \(n\leq 200\)时,我们枚举 \(1\) 可以出现的最靠后的位置,如果当前 \(1\) 在从右往左第 \(c\) 位,且需要摆放 \(b\) 个 \(1\),那么此时摆放的方案数为 \(\binom{c}{b}\),找到最小的 \(c\) 满足 \(\binom{c}{b}\geq 100\) ,然后枚举在后 \(c\) 位摆 \(b\) 个 \(1\) 的情况再排序即可。
当 \(n\geq 200\) 时,有 \(\binom{\frac{n}{2}}{1}\geq100\),答案只可能是 \(1\) 全部在 \(0\) 后面,或者最早的 \(1\) 出现后之后只有一个 \(0\)。
注意奇偶讨论的问题。具体见代码吧。
int n;
vector<int>val;
string lis[N];int tot;
void trans(){
++tot;
for(int i=1;i<n+1;++i){
lis[tot].push_back(val[i]==val[i-1]?'b':'r');
}
}
void letmake(int a,int b,int c,int d){
val.clear();
for(int i=1;i<=a;++i)val.push_back(0);
for(int i=1;i<=b;++i)val.push_back(1);
for(int i=1;i<=c;++i)val.push_back(0);
for(int i=1;i<=d;++i)val.push_back(1);
}
void check(int a,int b,int c){
letmake(a,b,c,0);
do{
trans();
}while(prev_permutation(val.begin(),val.end()));
}
int C(int n,int m){
int x=1;
for(int i=1;i<=m;++i){
x*=(n-i+1);
}
for(int i=2;i<=m;++i){
x/=i;
}
return x;
}
void share(int a,int b){
int x=b-1;
for(int i=0;i<b;++i){
if(C(a+i,i)>=200){
x=i;
break;
}
}
check(b-x,a,x);
}
int main(){
std::cin.sync_with_stdio(0);
std::cin.tie(0);
cin>>n;
if(n&1){
cout<<1ll*(n+1ll)*(n+1ll)/4ll<<"\n";
}else{
cout<<1ll*n*(n+2ll)/4ll<<"\n";
}
if(n>=300){
if(n&1){
int l=(n+1)/2;
letmake(l,l,0,0);trans();
for(int i=0;i<99;++i){
letmake(l-1,l-i,1,i);trans();
}
}else{
int a=n/2+1,b=n/2;
letmake(a,b,0,0);trans();
letmake(b,a,0,0);trans();
for(int i=0;i<98;++i){
letmake(a-1,b-i,1,i);trans();
}
}
//printf("%d\n",tot);
for(int i=1;i<=tot;++i)
cout<<lis[i]<<"\n";
return 0;
}
if(n&1){
int l=(n+1)/2;
share(l,l);
}else{
int a=n/2,b=n/2+1;
share(a,b);
share(b,a);
}
sort(lis+1,lis+1+tot);
for(int i=1;i<=min(100,tot);++i){
if(lis[i]==lis[i-1])assert(0);
cout<<lis[i]<<"\n";
}
return 0;
}
K. Scholomance Academy
\(n\) 个数据点,数据点实际分为两类(以 \(+-\) 区分),每个数据点有一个预测值 \(s\),一个机器按阈值 \(\theta\) 分类,\(s\geq \theta\) 时,数据被预测为 \(+\) 类,否则为 \(-\) 类。预测和实际的情况可写作下表。
实际类别\预测类别 \(+\) \(-\) \(+\) \(TP\) \(FN\) \(-\) \(FP\) \(TN\) 对每次按某个阈值分类求 \(4\) 种类别结果总数,记两个比例
\[TPR=\frac{TP}{TP+FN},FPR=\frac{FP}{TN+FP} \]现在要求 \(FPR\) 小于 \(r\) 时 \(TPR\) 能够达到的最大值对 \(r\) 积分的结果。形式化
\[AUC=\int_{0}^{1}\max_{\theta \in \R}\{TPR(\theta)|FPR(\theta)\leq r\}\text{d} r \]
读懂题后就不难了,显然可以对这 \(n\) 个数据 \(s\) 大小排序,小于 \(\theta\) 的一类,大于 \(\theta\) 的一类,预测的分类方法最多只有 \((n+1)\) 种。枚举这 \((n+1)\) 种,可以求得对应的 \(TPR,FPR\),然后按 \(FPR\) 排序后对 \(TPR\) 的前缀最大值积分(转化成若干个矩形面积和)即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int>ttfa;
const int N=1000006;
const ll INF=0x3f3f3f3f3f3f3f3f;
struct node{
int val,opt;
}a[N];
struct lin{
double pos,val;//pos<=r val
}b[N];int tot;
inline bool cmp(node x,node y){
return x.val<y.val;
}
inline bool cmp2(lin x,lin y){
return x.pos<y.pos;
}
int n;
array<int,2>sum[N];
int main(){
scanf("%d",&n);
char ins[4];
for(int i=1;i<=n;++i){
scanf("%s%d",ins,&a[i].val);
if(ins[0]=='+')a[i].opt=1;
else a[i].opt=0;
}
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;++i){
sum[i][0]=sum[i-1][0];
sum[i][1]=sum[i-1][1];
sum[i][a[i].opt]++;
}
sum[n+1]=sum[n];
for(int i=1;i<=n+1;++i){
if(a[i].val==a[i-1].val)continue;
double tp=sum[n+1][1]-sum[i-1][1],fn=sum[i-1][1];
double fp=sum[n+1][0]-sum[i-1][0],tn=sum[i-1][0];
b[++tot].pos=fp/(fp+tn);
b[tot].val=tp/(tp+fn);
}
sort(b+1,b+1+tot,cmp2);
double maxv=0,ans=0;
for(int i=1;i<tot;++i){
maxv=max(maxv,b[i].val);
ans+=maxv*(b[i+1].pos-b[i].pos);
}
printf("%.10lf\n",ans);
return 0;
}
H. The Boomsday Project
单车租赁每次需付 \(r\) 元。可购买 \(n\) 种折扣卡:第 \(i\) 种有 \(k_i\) 次免费租车、有效期 \(d_i\) 天,价格 \(c_i\) 元。卡从购买当天起生效,至 \(t+d_i-1\) 天结束。新卡会覆盖旧卡(剩余免费次数作废)。每种卡可无限购买。给定租车记录(\(m\) 对表示 \(p_i\) 天租 \(q_i\) 次给出),求最小花费。
\(n\leq 500,\sum q_i\leq 3\times 10^5\)
将 \(m'=\sum q_i\) 辆车列举出来按日期排序,设 \(dp_{i}\) 表示租到 \(1\sim i\) 这些车的最小花费。如果不用卡,有 \(dp_{i}\leftarrow dp_{i-1}+r\)。
考虑第 \(i\) 辆车使用第 \(j\) 张租车卡,由于我们不关注 \(i\) 之后的车,于是我们让这张卡在这着一辆车过期,看往前能用这张卡能租到多少辆车,设最近的不能租到的位置为 \(pos_j\),则 \(dp_{i}\leftarrow dp_{pos_j}+c_j\)。
显然随 \(i\) 的增大,\(pos_j\) 也是增大的,于是我们可以动态维护 \(pos_j\)。时间复杂度 \(O(n\sum q)\) 。具体实现见代码。
ll dp[MAXM];
int n, m, r, d[MAXN], k[MAXN], c[MAXN], s[MAXM], pre[MAXN];
int main() {
n=read(9), m=read(9), r=read(9);
for(int i=1; i<=n; ++i)
d[i]=read(9), k[i]=read(9), c[i]=read(9);
int cnt = 0;
for(int i=1; i<=m; ++i) {
int p=read(9), q=read(9);
while(q--) s[++cnt] = p;
}
sort(s+1, s+cnt+1);
for(int i=1; i<=cnt; ++i) {
dp[i] = dp[i-1]+r;
for(int j=1; j<=n; ++j) {
while(s[i]-s[pre[j]+1] >= d[j] || i-pre[j] > k[j]) ++ pre[j];
dp[i] = min(dp[i], dp[pre[j]]+c[j]);
}
}
print(dp[cnt], '\n');
return 0;
}
I. Rise of Shadows
一天有 \(H\) 小时,每小时 \(M\) 分钟。初始时两针重合,时针每 \(H\) 小时转一圈,分针每 \(M\) 分钟转一圈。定义 \(\alpha = \frac{2\pi A}{HM}\)。求有多少个整数分钟时刻(从一天开始计),使得两针夹角 \(\le \alpha\)。
输入:一行三个整数 \(H, M\)(\(2 \le H, M \le 10^9\))和 \(A\)(\(0 \le A \le \frac{HM}{2}\))。
赛时止步于此,此题可参见独立博客题解。
C. Mean Streets of Gadgetzan
给出若 \(n\) 个命题,每个命题要么为真要么为假,现给出若干条正确的条件,每个条件为以下四种情况之一:
- 某个命题为真
x- 某个命题为假
!x- 若某些命题都为真,则某个命题为真
y1 y2 ... yk -> x- 若某些命题都为假,则某个命题为假
y1 y2 ... yk -> !x现在请根据已知条件推断所有命题的真假性,或判断不可能。
\(n\leq 10^6\)
是不是很多人都往 2-sat 想了所以赛时过的不多,感觉不算很难。
因为第 \(3,4\) 种条件都只需要一些真命题来推断(而不是真假参杂),所以如果最后一些命仍然未确定真假,直接全认定为假并不会导致矛盾。
当我们判定一个命题为真命题后,看看哪些命题会由他推导出,如果这些命题只差这个命题就能推导出来了,那么就可以得出该推断正确,并将这个判断出来的命题在之后继续参与别的推断的判定。
当我们判定一个命题为假命题后,那他就是假命题,且所有由他推导出来的命题也会因他缺失而无法得到。
只需要对每个推断命题求一下需要 \(siz\) 个真命题推断,然后其中一个真命题满足了就给计数器 \(num:=num+1\),如果计数器 \(num=siz\) 就说明推断成立。具体实现见代码。
int n,m;
vector<int>ins;
string s;
void readline(){
getline(cin,s);
int x=0,f=1;
ins.clear();
for(auto ch:s){
if(ch<'0'||'9'<ch){
if(x)ins.push_back(f*x);
x=0,f=1;
if(ch=='!')f=-1;
continue;
}
if('0'<=ch&&ch<='9'){
x=x*10+ch-'0';
}
}
if(x)ins.push_back(f*x);
}
queue<int>q;
vector<int>lis[N];
vector<int>tar[N];
int ans[N],had[N],siz[N];
int main(){
std::cin.sync_with_stdio(0);
std::cin.tie(0);
readline();
m=ins[0],n=ins[1];
for(int i=1;i<=m;++i){
readline();
int len=ins.size();
lis[i]=ins;siz[i]=len-1;
if(len==1){
q.push(ins[0]);
}else{
for(int j=0;j<len-1;++j){
tar[ins[j]].push_back(i);
}
}
}
while(!q.empty()){
int u=q.front();q.pop();
if(u>0){
if(ans[u]==-1){ans[0]=-1;break;}
if(ans[u]==1)continue;
ans[u]=1;
for(auto id:tar[u]){
++had[id];
if(had[id]==siz[id]){
q.push(lis[id][siz[id]]);
}
}
}else{
u=-u;
if(ans[u]==1){ans[0]=-1;break;}
if(ans[u]==-1)continue;
ans[u]=-1;
}
}
if(ans[0]==-1){puts("conflict");return 0;}
for(int i=1;i<=n;++i){
if(ans[i]==1)printf("T");
else printf("F");
}puts("");
return 0;
}
M. United in Stormwind
有 \(m\) 个问题,每个问题答案 A 或 B。给定 \(n\) 份调查结果(长度为 \(m\) 的字符串)。一个非空问题子集称为可区分的,当且仅当存在至少 \(k\) 对结果,它们在该子集的至少一个问题上答案不同。求可区分子集的数量。
输入:\(n, m, k\)(\(1 \le n \le 2\times10^5\),\(1 \le m \le 20\),\(1 \le k \le \frac{n(n-1)}{2}\)),接下来 \(n\) 行每行一个长度为 \(m\) 的字符串。
由于根本想不到怎么做,本题解大量参考大佬的题解,因为我觉得写得特别好,请见谅。
首先调查结果转化成长度为 \(m\) 的二进制数。将两个数异或起来为 \(1\) 的位置就是两个结果不同的位置集合。可以使用 FWT 求每种异或值的对数,同时注意去重与自身配对情况。
然后对于一个问题子集 \(S\),只要异或值的某一位在该子集上为 \(1\) 就可计数。这东西不好作,我们考虑求补集,即所有的情况数 \(\binom{n}{2}\) 减去异或值在子集的每一位上全 \(0\) 的情况数。
子集的每一位全为 \(0\),而其他位任选;我们求出 \(S\) 的补集 \(T\)(\(01\) 翻转),相当于 \(T\) 上的每一为 \(1\) 的位可以任选 \(01\),为 \(0\) 的位只能选 \(0\),也就是求 \(T\) 的子集和。这个东西可以使用高维前缀和(SOS dp)来做。
const int N=22,M=2000006;
ll k,a[M],n,m;
void fwt_xor(ll *f,ll opt){
for(int len=2,mid=1;len<=(1<<n);len<<=1,mid<<=1){
for(int i=0;i<(1<<n);i+=len){
for(int j=0;j<mid;++j){
ll tmp1=f[i+j],tmp2=f[i+j+mid];
f[i+j]=tmp1+tmp2;
f[i+j+mid]=tmp1-tmp2;
if(opt<0)f[i+j]/=2,f[i+j+mid]/=2;
}
}
}
}
int main(){
scanf("%lld%lld%lld",&m,&n,&k);
char t[N];
for(int i=1;i<=m;++i){
scanf("%s",t);int v=0;
for(int j=0;j<n;++j){
if(t[j]=='A')v+=(1<<j);
}
++a[v];
}
fwt_xor(a,1);
for(int i=0;i<(1<<n);++i)a[i]=a[i]*a[i];
fwt_xor(a,-1);
a[0]-=m;
for(int i=0;i<(1<<n);++i)a[i]/=2;
for(int i=0;i<n;++i){//这个循环次序才是高维前缀和,否则会算重
for(int j=0;j<(1<<n);++j){
if((j>>i)&1)a[j]+=a[j^(1<<i)];
}
}
ll tot=m*(m-1)/2,ans=0;
for(int i=1;i<(1<<n);++i){
if(tot-a[i^((1<<n)-1)]>=k)
++ans;
}
printf("%lld\n",ans);
return 0;
}
E. Knights of the Frozen Throne
有 \(m\) 个玩家,每个玩家有若干请求时间(递增)。缓存策略:玩家数据在内存中保留 \(X\) 秒(从最后一次请求起算),超时则被清除。初始内存为空。请求时:若数据不在内存,则加载(成本为 \(i \cdot b_i\),其中 \(i\) 为该秒内加载的玩家数),并更新最后请求时间;若已在内存,则直接响应并更新时间。内存持有成本为每玩家每秒 \(a\) 元。求使总成本最小的正整数 \(X\),输出最小总成本、达到最小成本的 \(X\) 的个数,以及所有 \(X\)(升序)。
输入:第一行 \(m\)。接下来 \(m\) 行,每行先给 \(k\),再给 \(k\) 个递增时间。下一行 \(a\),最后一行 \(b_1, b_2, \dots, b_m\)。保证总请求数 \(\le 5\times 10^5\)。
对于同一名玩家,我们只关注时间相邻的请求(因为每次请求后都会更新时间)。
对某个 \(X\) 求内存持有成本较为容易,将每个请求到该玩家的下一名请求的时间间隔 \(gap\) 从小到达排序,如果 \(X\) 大于时间间隔 \(gap\),则内存对该请求的持有时间为 \(gap\),否则为 \(X\)。对于最后一个请求,我们认为 \(gap=\infty\)。可以发现这部分花费随 \(X\) 增大严格增大。
对某个 \(X\) 求所有时刻的 \(i\cdot b_i\) 虽更为复杂也不难。我们只需要看 \(X\) 变大到何值时,该时刻需要重新写入内存的数据会减少,当 \(X\) 增大为某个玩家距上一个请求的时间间隔 \(gap+1\) 时,该时刻写入内存的数据量会 \(-1\)。按这个 \(gap\) 排序后即可即可求出当 \(X\) 增大到多少写入花费为多少,进而算出较原先减少多少。因为 \(gap\) 最有请求数总和个,于是只需要枚举这些个 \(X\)(另一部花费严格增大保证)。
具体实现见代码
int n;ll b[N],c;
vector<int>p[N],a[N];
int t[N],m;
array<ll,2>lis[N];int tot;
ll gap[N],sum[N],num;
ll minv;
vector<int>kval;
ll queryc(ll k){
int loc=lower_bound(gap+1,gap+1+num,k)-gap;
return (sum[loc-1]+(num-loc+1)*k)*c;
}
int main(){
std::cin.sync_with_stdio(0);
std::cin.tie(0);
cin>>n;
for(int i=1;i<=n;++i){
int k;cin>>k;
for(int j=1;j<=k;++j){
int tim;cin>>tim;
t[++m]=tim;
p[i].push_back(tim);
}
p[i].erase(unique(p[i].begin(),p[i].end()),p[i].end());
}
cin>>c;
for(int i=1;i<=n;++i)cin>>b[i];
sort(t+1,t+1+m);
m=unique(t+1,t+1+m)-t-1;
for(int i=1;i<=n;++i){
//统计某时刻加入的人有哪些(加入代价 b)
for(auto tim:p[i]){
int loc=lower_bound(t+1,t+1+m,tim)-t;
a[loc].push_back(i);
}
//统计对同一人的间隔时刻(存储代价 c)
for(int j=0;j<(int)p[i].size()-1;++j){
gap[++num]=p[i][j+1]-p[i][j];
}gap[++num]=2000000000-p[i][p[i].size()-1];//我还以为最后一个存储进去后就不算了呢
}
ll sumb=0;
for(int i=1;i<=m;++i){
vector<ll>del;
for(auto j:a[i]){
int loc=lower_bound(p[j].begin(),p[j].end(),t[i])-p[j].begin();
if(loc==0){
del.push_back(1000000005);
}else{
del.push_back(p[j][loc]-p[j][loc-1]+1);
}
}
ll las=(ll)a[i].size()*b[a[i].size()];
sumb+=las;
sort(del.begin(),del.end());
for(int j=0;j<(int)a[i].size();++j){
ll now=(ll)(a[i].size()-j-1)*b[a[i].size()-j-1];
lis[++tot]={del[j],now-las};
las=now;
}
}
sort(lis+1,lis+1+tot);
sort(gap+1,gap+1+num);
for(int i=1;i<=num;++i)sum[i]=gap[i]+sum[i-1];
//统计答案
minv=sumb+queryc(1);kval.push_back(1);
//printf("1 %lld %lld %lld\n",minv,sumb,minv-sumb);
for(int i=1,k=1;i<=tot;++i){
k=lis[i][0],sumb+=lis[i][1];
if(lis[i][0]==lis[i+1][0]||k>1000000000)continue;
ll tmp=sumb+queryc(k);
//printf("%d %lld %lld %lld\n",k,tmp,sumb,tmp-sumb);
if(tmp<minv){
minv=tmp;
kval.clear();
kval.push_back(k);
}else if(tmp==minv){
kval.push_back(k);
}
}
printf("%lld %d\n",minv,(int)kval.size());
for(auto x:kval)printf("%d ",x);
puts("");
return 0;
}
感觉整场思维难度不算很大,但是实现起来还是好多锅qwq。另外还有个 A 题也可以补但是我不会计算几何,有时间就学。

浙公网安备 33010602011771号