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\) 个命题,每个命题要么为真要么为假,现给出若干条正确的条件,每个条件为以下四种情况之一:

  1. 某个命题为真 x
  2. 某个命题为假 !x
  3. 若某些命题都为真,则某个命题为真 y1 y2 ... yk -> x
  4. 若某些命题都为假,则某个命题为假 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 题也可以补但是我不会计算几何,有时间就学。

posted @ 2026-04-12 13:35  BigSmall_En  阅读(42)  评论(0)    收藏  举报