zoukankan      html  css  js  c++  java
  • CCPC 2020 长春站 部分简略题解

    gym链接:CCPC 2020 changchun site

    A:

    题目大意:商店里有若干个充值档位和首充奖励,你有(n)块钱问最多能拿到多少水。

    解:由于档位不多可以直接枚举,整个二进制枚举一下所有方案就可以了。不要陷入贪心的方向,因为可能买便宜的会更多。

    代码:

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define forn(i,x,n) for(int i = x;i <= n;++i)	
    
    int v[10],e[10];
    int main()
    {
    	int n;scanf("%d",&n);
    	v[0] = 1,v[1] = 6,v[2] = 28,v[3] = 88,v[4] = 198,v[5] = 328,v[6] = 648;
    	e[0] = 8,e[1] = 18,e[2] = 28,e[3] = 58,e[4] = 128,e[5] = 198,e[6] = 388;
    	int res = 0;
    	for(int i = 0;i < (1 << 7);++i)
    	{
    		int t = 0,s = 0;
    		for(int j = 0;j < 7;++j)
    			if(i >> j & 1)
    				s += v[j];
    		if(s > n)	continue;
    		for(int j = 0;j < 7;++j)
    			if(i >> j & 1)
    				t += e[j];
    		res = max(res,t + n * 10);
    	}
    	printf("%d",res);
        return 0;
    }
    
    

    D:

    题目大意:构造一个序列({a}),满足(a_0=1,a[n] = c * max_{i in [0,n - 1]}a[i])。求(sumlimits_{i=0}^na_i \% (10^9+7))

    解:枚举之后可以发现(a_i = c^{popcount(i)})。剩下的问题就是求和。由于长度(leq 3000),显然应该枚举每一位的可能,那么当本位是(0)的时候显然只能填(0),如果是(1)则既可以填(0)也可以填(1),往后继续讨论,如果本位填了(0)则之后的所有位都可以填任意的数字,并且贡献只和(1)的数量有关,不难想到之后的贡献与长度(L)有关,也就是(sum C_L^i * c^i),其中(i)表示填入(1)的个数,同时还与前面填过有多少个(1)相关。想到这里整个解差不多可以明确出来了:枚举前缀,当本位是(1)的时候计算后面任意填的贡献,并累加之前前缀的(1)的个数。整个算法复杂度是长度的平方,可以过掉。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define forn(i,x,n) for(int i = x;i <= n;++i)	
    
    const int N = 3007,MOD = 1e9+7;
    char s[N];
    ll C[N][N];
    void init()
    {
    	forn(i,0,N - 1)
    		forn(j,0,i)
    		{
    			if(j == 0)	C[i][j] = 1;
    			else		C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % MOD;
    		}
    }
    int main()
    {
    	init();
    	scanf("%s",s + 1);int n = strlen(s + 1),c;scanf("%d",&c);
    	ll res = 0,fact = 1,cnt1 = 0;
    	forn(i,1,n)
    	{
    		if(s[i] == '0')	continue;
    		else
    		{
    			int L = n - i;
    			ll p = 0,pc = 1;
    			forn(j,0,L)
    			{
    				p = (p + C[L][j] * pc % MOD) % MOD;
    				pc = pc * c % MOD;
    			}
    			res = (res + fact * p % MOD) % MOD;
    			fact = fact * c % MOD;
    			++cnt1;
    		}
    	}
    	res = (res + fact) % MOD;
    	printf("%lld",res);
        return 0;
    }
    
    

    F:

    题目大意:给定一颗树,求(sumlimits_{i=1}^nsumlimits_{j=i+1}^n[a_i oplus a_j = a_{lca(i,j)}](ioplus j))

    解:一个显然的想法是,枚举所有点作为(lca(i,j))的情形,然后再去寻找(i,j)。显然这样的两个点应该处于这个点的不同的子树中,否则(lca)就变了。由于问题是静态的,并且需要整个子树的信息,可以联想到树上启发式合并,考虑使用(set)记录子树中权值对应的点的编号。树上启发式合并的过程:

    (1)向下递归所有(u)的轻儿子,并且做完他们的过程之后把对应的(set)清空。

    (2)向下递归(u)的重儿子,把重儿子的答案算完后保留对应的(set)

    (3)再次递归所有(u)的轻儿子,计算所有轻儿子关于当前所有其他已经加入(set)中点的贡献,计算完后再把整个轻儿子的子树的信息加入(set)

    更具体的来说,树上启发式合并的过程是先递归把儿子的答案都计算好,(2)时保留重儿子的信息,之后再慢慢把轻儿子的信息也都加入,但是加入轻儿子的过程中,必须要先计算后加入,因为可能会出现同一颗子树中产生贡献的情况。显然这个做法的时间复杂度是(O(nlog^2n))的,这个做法的时间复杂度还不够优,会被T掉。考虑优化掉(set)的一层(log)

    这里有一个不正确但是AC的做法:由于每次(set)的插入和删除的是一段连续的序列,所以直接把(set)换成(vector),利用连续性直接(pushback)(popback)就可以正确维护信息了。但是这个做法在树上权值大量相同的时候会退化到平方复杂度。

    考虑用静态的数组踢掉(vector)的插入删除,本来做不了的原因是权值的编号不同,思路比较显然就是把编号的贡献也拆到位上,只有当两个位上都不同的时候才会有贡献,于是可以按这个思路把每个权值下,对应位数上记录累加了几次,删除的时候直接对本位删一次就可以了。

    在具体实现上,需要记录往下有哪些重儿子已经被记录过了,在计算删除的时候也要看是不是之前还没退出来的重儿子,需要打一个标记记录并跳过某些重儿子。

    // Problem: F. Strange Memory
    // Contest: Codeforces - 2020 China Collegiate Programming Contest - Changchun Site
    // URL: https://codeforces.com/gym/102832/problem/F
    // Memory Limit: 256 MB
    // Time Limit: 1000 ms
    // Powered by CP Editor (https://github.com/cpeditor/cpeditor)
    
    
    #define _CRT_SECURE_NO_WARNINGS
    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <set>
    #include <vector>
    using namespace std;
    typedef long long ll;
    #define forn(i,x,n) for(int i = x;i <= n;++i)	
    
    const int N = 1e5+7,M = 2 * N,NC = 1e6+7;
    vector<int> tb[NC];
    int ver[N],succ[M],edge[M],idx;
    int a[N],n,sz[N],son[N];
    int cnt[(1 << 20) + 7][23][2];
    bool st[N];
    ll res = 0;
    void add(int u,int v)
    {
    	edge[idx] = v;
    	succ[idx] = ver[u];
    	ver[u] = idx++;
    }
    
    void update(int id,int val)
    {
    	for(int i = 0;i < 20;++i)
    		cnt[a[id]][i][id >> i & 1] += val;
    }
    
    ll find(int id,int num)
    {
    	ll res = 0;
    	for(int i = 0;i < 20;++i)
    		res += cnt[num][i][!(id >> i & 1)] * (1 << i);
    	return res;
    }
    
    
    void dfs1(int u,int fa)
    {
    	sz[u] = 1;
    	for(int i = ver[u];~i;i = succ[i])
    	{
    		int v = edge[i];
    		if(v == fa)	continue;
    		dfs1(v,u);
    		sz[u] += sz[v];
    		if(sz[son[u]] < sz[v])	son[u] = v;
    	}
    }
    
    void calc(int u,int fa,int lca)
    {
    	if((a[u] ^ lca) < NC)	res += find(u,a[u] ^ lca);
    
    	for(int i = ver[u];~i;i = succ[i])
    	{
    		int v = edge[i];
    		if(v == fa || st[v])	continue;
    		calc(v,u,lca);
    	}
    }
    
    void insert(int u,int fa)
    {
    	update(u,1);
    	for(int i = ver[u];~i;i = succ[i])
    	{
    		int v = edge[i];
    		if(v == fa || st[v])	continue;
    		insert(v,u);
    	}
    }
    
    void remove(int u,int fa)
    {
    	update(u,-1);
    	for(int i = ver[u];~i;i = succ[i])
    	{
    		int v = edge[i];
    		if(v == fa || st[v])	continue;
    		remove(v,u);
    	}
    }
    
    void dfs2(int u,int fa,int keep)
    {
    	for(int i = ver[u];~i;i = succ[i])
    	{
    		int v = edge[i];
    		if(v == fa || v == son[u])	continue;
    		dfs2(v,u,0);
    	}
    	if(son[u])	dfs2(son[u],u,1),st[son[u]] = 1;
    	update(u,1);
    	for(int i = ver[u];~i;i = succ[i])
    	{
    		int v = edge[i];
    		if(v == fa || st[v])	continue;
    		calc(v,u,a[u]);
    		insert(v,u);
    	}
    	if(son[u])	st[son[u]] = 0;
    	if(!keep)	remove(u,fa);
    }
    
    int main()
    {
    	memset(ver,-1,sizeof ver);
    	scanf("%d",&n);
    	forn(i,1,n)	scanf("%d",&a[i]);
    	forn(i,1,n-1)
    	{
    		int u,v;scanf("%d%d",&u,&v);
    		add(u,v),add(v,u);
    	}
    	dfs1(1,-1);
    	dfs2(1,-1,1);
    	printf("%lld",res);
    	return 0;
    }
    

    H:

    题目大意:有一个最多五位的密码锁,上面的数字都是(0-9),每次可以拨动上面的一位(正反旋转都是允许的)。两个人轮流拨锁,当某个人转到一个之前有过的密码,或者是某个两方开始之前制定的某个密码时判输。问是否是先手必胜的。

    解:假如把所有状态的和的奇偶性组织起来,那么每次状态之间的移动和会从奇数变成偶数,从偶数变成奇数,那么就可以构造成一个二分图。由于不能往已经走过的状态走,那么当起点在二分图的最大匹配的必须点上时(不妨说必须点在左侧,左部右部交换不影响正确性),而对手一定可以沿着最大匹配把状态移动回左部,并且移动过的边数一定是偶数,于是先手必败。

    建图比较显然,先记录有哪些点是禁止的,没有影响的就按位枚举转移,每条边的流量都是(1)。不妨先不加入起始状态的边,做一次最大流,再把起始状态的边加入再求一次最大流,假如产生了增广,那么就说明起点是一个必须点,那么先手必败,反之先手必胜。

    #define _CRT_SECURE_NO_WARNINGS
    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define forn(i,x,n) for(int i = x;i <= n;++i)	
    
    const int N = 1e6+7,M = 2 * N,INF = 1 << 29;
    int forb[N],fact[] = {1,10,100,1000,10000,100000};
    int m,n,s,t,CASE,STR;
    int edge[M],succ[M],cap[M],ver[N],idx;
    int d[N],pre[N],now[N];
    int q[M],hh,tt;
    void add(int u,int v,int w)
    {
    	edge[idx] = v;
    	cap[idx] = w;
    	succ[idx] = ver[u];
    	ver[u] = idx++; 
    	
    	edge[idx] = u;
    	cap[idx] = 0;
    	succ[idx] = ver[v];
    	ver[v] = idx++;
    }
    bool bfs()
    {
    	memset(d,0,sizeof d);
    	hh = 0,tt = -1;
    	q[++tt] = s;d[s] = 1;now[s] = ver[s];
    	while(hh <= tt)
    	{
    		int u = q[hh++];
    		for(int i = ver[u];~i;i = succ[i])
    		{
    			int v = edge[i];
    			if(cap[i] && !d[v])
    			{
    				q[++tt] = v;
    				now[v] = ver[v];
    				d[v] = d[u] + 1;
    				if(v == t)	return 1;
    			}
    		}
    	}
    	return 0;
    }
    int dinic(int u,int limit)
    {
    	if(u == t)	return limit;
    	int flow = 0,k;
    	for(int i = now[u];~i && flow < limit;i = succ[i])
    	{
    		now[u] = i;
    		int v = edge[i];
    		if(cap[i] && d[v] == d[u] + 1)
    		{
    			k = dinic(v,min(limit - flow,cap[i]));
    			if(!k)	d[v] = -1;
    			cap[i] -= k;
    			cap[i ^ 1] += k;
    			flow += k;
    		}
    	}
    	return flow;
    }
    int dinic()
    {
    	int res = 0,flow = 0;
    	while(bfs())
    		while(flow = dinic(s,INF))
    			res += flow;
    	return res;
    }
    
    int main()
    {
    	ios::sync_with_stdio(0);cin.tie(0);
        int T;cin >> T;
        for(int CASE = 1;CASE <= T;++CASE)
        {
    		memset(ver,-1,sizeof ver);idx = 0;
    		cin >> m >> n >> STR;
    		forn(i,1,n)
    		{
    			int x;cin >> x;
    			forb[x] = CASE;
    		}
    		s = fact[5] + 1,t = s + 1;
    		// for(int i = ver[4];~i;i = succ[i])	cout << edge[i] << endl;
        	for(int i = 0;i < fact[m];++i)
        	{
        		if(forb[i] == CASE)	continue;
        		int sum = 0,x = i;
        		while(x)
        		{
        			sum += x % 10;
        			x /= 10;
        		}
        		// cout << i << " " <<s << endl;
        		if(sum % 2 == 1)	{if(i != STR)	add(i,t,1);}
        		else
        		{
        			// cout << i << endl;
        			if(i != STR)	add(s,i,1);
        			// cout << i << endl;
                    for(int j=0;j<m;j++)
                    {
                        int num=i;
                        for(int k=0;k<j;k++)num/=10;
                        num=num%10;
                        int k=(num+1)%10*fact[j];
                        k=i-num*fact[j]+k;
                        if(k < fact[m])add(i,k,1);
                        k=(num-1+10)%10*fact[j];
                        k=i-num*fact[j]+k;
                        if(k < fact[m])add(i,k,1);
                    }
        		}
        	}
        	ll res = dinic();
        	int sum = 0;
        	int __ = STR;
        	while(__)
        	{
        		sum += __ % 10;
        		__ /= 10;
        	}
        	if(sum % 2 == 1)	add(STR,t,1);
        	else				add(s,STR,1);
        	ll res_r = dinic();
        	// cout << res << " " << res_r << endl;
        	if(!res_r)	cout << "Bob
    ";
        	else		cout << "Alice
    ";
        }
        return 0;
    }
    
    

    J:

    题目大意:有一个长度为(n)的横线,一开始有若干个圆已经在图上,你可以增加若干个半径不超过(5)的圆,并且任意两个圆之间不能有超过(1)个交点,问有多少种不同的方案。数据保证最开始的圆都是合法的。

    解:看到题面之后可以想到应该是一个(dp)问题,决策很多方案也很多。同时可以看到这个决策是与一段区间相关的,不妨先考虑简化的问题:假设一开始没有画上若干个圆,直接对应求方案数。状态:(f[l][r][0])表示在([l,r])这个位置上不存在恰好一个圆的左端点处在(l)且右端点处在(r)的方案数,(f[l][r][1])表示恰好有一个圆处在([l,r])这个位置上的方案数。

    入口:显然对于(f[i][i+1][0]=1)(f[i][i+1][1]=0)

    转移:先考虑存在一个恰好的圆处在([l,r])的时候,当长度在(10)以内且是偶数的时候,有(f[l][r][1] = f[l][r][0])。另外一种情况下,考虑状态的分界点,对于最左侧的端点(l)要么旁边没有圆,要么直接相切某个半径是(k)的圆,采用这种转移的原因是(k)的取值只有(5)种。如果(l)身边没有任何一个圆,那么就是(f[l][r][0] += f[l + 1][r][0] + f[l + 1][r][1])。假设身边相切一个半径是(k)的圆,那么就是(f[l][r][0] += f[l][l + 2k][1] * (f[l +2k][r][0] + f[l + 2k][r][1]))

    出口:(f[0][n][0] + f[0][n][1])

    非法状态:再加回原来存在的圆之后,对于某一个([l,r])范围的圆,那么对于内部的坐标(jin[l+1,r-1])不能有任何一个圆的覆盖位置是([k,j])([j,k])其中(kin[0,l-1])(kin[r+1,n])。标记这样的位置,这样的区间不能有任何方案数,其次(f[l][r][0] = 0)

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define forn(i,x,n) for(int i = x;i <= n;++i)	
    
    const int N = 2e3+7,MOD = 1e9+7;
    int f[N][N][2],forb[N][N],cir[N][N];
    int n,k;
    int main()
    {
    	scanf("%d%d",&n,&k);
    	forn(i,1,k)
    	{
    		int r,c;scanf("%d%d",&c,&r);
    		cir[c - r][c + r] = 1;
    		forn(j,c - r + 1,c + r - 1)
    		{
    			forn(k,0,c - r - 1)	forb[k][j] = 1;
    			forn(k,c + r + 1,n)	forb[j][k] = 1;
    		}
    	}
    	forn(i,0,n - 1)	f[i][i + 1][0] = 1,f[i][i + 1][1] = 0;
    	forn(len,2,n)
    	{
    		for(int l = 0;l + len <= n;++l)
    		{
    			int r = l + len;
    			if(forb[l][r])
    			{
    				f[l][r][1] = 0;
    				f[l][r][0] = 0;
    				continue;
    			}
    			int& v = f[l][r][0];
    			v = (1ll*v + f[l + 1][r][0] + f[l + 1][r][1]) % MOD;
    			for(int k = l + 2;k <= l + 10 && k <= r;k += 2)
    				v = (v + f[l][k][1] * (1ll* f[k][r][0] + f[k][r][1])) % MOD;
    			if(len <= 10 && len % 2 == 0)	f[l][r][1] = f[l][r][0];
    			if(cir[l][r])	f[l][r][0] = 0;
    		}
    	}
    	printf("%lld",(1ll* f[0][n][0] + f[0][n][1]) % MOD);
        return 0;
    }
    

    K:

    题目大意:一开始有若干颗只有一个点的树,之后有若干个操作,每次操作是下面3种:

    (1)增加一个只有一个点的树

    (2)链接(x,y)两个点所在的树,如果两个点一开始就在一个树里,则不进行操作

    (3)直接对编号为(x)的点修改他的权值

    每个操作之后输出所有树里满足(a_i oplus a_j=gcd(a_i,a_j))的数对的总个数。

    解:由于要合并两棵树,并且要知道树里的结点的具体个数,不难想到使用(set<pii>){权,个数}的方式表示一棵树。之后对于链接两棵树的过程通过并查集维护根以及(set)的启发式合并就可以使复杂度掉到(O(nlog^2n))。看起来比较可做,继续考虑题目统计的条件。

    对于(a oplus b = gcd(a,b))的条件,不妨假设(a < b),左边的异或有(a oplus b geq b - a),证明可以通过把数拆成位和,并且规约到每一位的情况解决。对于右边的(gcd(a,b)=gcd(a,b-a) leq b - a)组合起来可以发现如果两边相等则必然有(b-a | a),当然有这个条件不能说明一定有亦或的值也相等,但是这至少说明了(b-a)(a)的关系,并启发可以通过枚举约数及其倍数的方式得到所有可能,但是还是要判断一下是不是异或值相等的。通过打表之后可以发现对于任意一个(a)他合法的(b)的个数很少,于是进而可以想到暴力解决这个问题。

    对于每个(2)操作,我们枚举较小的树中所有权值,并在较大的树中查询是否有需要的权值及其个数,计算完对答案的增量之后再把较小的所有元素插入到较大的中去。对于每个(3)操作,不妨先删掉原有的值,再插入一个新的直接应用修改。

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    #define forn(i,x,n) for(int i = x;i <= n;++i)	
    typedef pair<int,int> pii;
    #define x first
    #define y second
    
    const int N = 1e5+7,M = 2e5+7;
    set<pii> tr[N + M];
    vector<int> v[M + N];
    int a[N + M],fa[N + M],idx;
    int n,m;
    ll res;
    
    int find(int x)
    {
    	if(x == fa[x])	return x;
    	return fa[x] = find(fa[x]);
    }
    
    ll calc(int x,int u,int cnt)
    {
    	ll r = 0;
    	for(auto& tar : v[u])
    	{
    		auto it = tr[x].lower_bound({tar,0});
    		if(it == tr[x].end())	continue;
    		if((*it).x == tar)	r += 1ll * (*it).y * cnt;
    	}
    	return r;
    }
    
    
    void merge(int x,int y)
    {
    	int fx = find(x),fy = find(y);
    	if(fx == fy)	return ;
    	if(tr[fx].size() > tr[fy].size())	swap(fx,fy);
    	fa[fx] = fy;
    	for(auto& u : tr[fx])	res += calc(fy,u.x,u.y);
    	for(auto& u : tr[fx])
    	{
    		auto it = tr[fy].lower_bound({u.x,0});
    		if(it == tr[fy].end())	tr[fy].insert(u);
    		else
    		{
    			if((*it).x == u.x)	tr[fy].erase(it),tr[fy].insert({u.x,u.y + (*it).y});
    			else	tr[fy].insert(u);
    		}
    	}
    }
    
    void update(int x,int v)
    {
    	int fx = find(x);
    	auto it = tr[fx].lower_bound({a[x],0});
    	int num = (*it).y;
    	tr[fx].erase(it);
    	if((*it).y > 1)	tr[fx].insert({a[x],num - 1});
    	res -= calc(fx,a[x],1);
    	
    	a[x] = v;
    	res += calc(fx,a[x],1);
    	it = tr[fx].lower_bound({a[x],0});
    	if(it == tr[fx].end())	tr[fx].insert({a[x],1});
    	else
    	{
    		num = (*it).y;
    		if((*it).x == a[x])	tr[fx].erase(it),tr[fx].insert({a[x],num + 1});
    		else	tr[fx].insert({a[x],1});
    	}
    }
    
    
    int main()
    {
    	scanf("%d%d",&n,&m);
    	forn(i,1,n)	scanf("%d",&a[i]),tr[i].insert({a[i],1});
    	//init
    	for(int d = 1;d <= 200000;++d)
    		for(int a = 2 * d;a <= 200000;a += d)
    		{
    			int b = d + a;
    			if(b > 200000 || (a ^ b) != b - a)	continue;
    			v[a].push_back(b);
    			v[b].push_back(a); 
    		}
    	forn(i,1,n + m)	fa[i] = i;
    	
    	while(m--)
    	{
    		int t;scanf("%d",&t);
    		if(t == 1)
    		{
    			int x,v;scanf("%d%d",&x,&v);
    			a[x] = v;
    			tr[x].insert({v,1});
    		}
    		else if(t == 2)
    		{
    			int x,y;scanf("%d%d",&x,&y);
    			merge(x,y);
    		}
    		else
    		{
    			int x,v;scanf("%d%d",&x,&v);
    			update(x,v);
    		}
    		printf("%lld
    ",res);
    	}
        return 0;
    }
    
    
  • 相关阅读:
    webpack之理解loader
    React中需要多个倒计时的问题
    react.js中实现tab吸顶效果问题
    利用浏览器调试APP中的H5页面
    css一长串连续英文字符的换行
    纯css实现移动端横向滑动列表
    javascript数据结构——写一个二叉搜索树
    javascript数据结构——链表
    javascript数组去重
    《正则表达式必知必会(修订版)》笔记
  • 原文地址:https://www.cnblogs.com/HotPants/p/13965473.html
Copyright © 2011-2022 走看看