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;
    }
    
    
  • 相关阅读:
    leetcode 29-> Divide Two Integers without using multiplication, division and mod operator
    ros topic 发布一次可能会接收不到数据
    python中的print()、str()和repr()的区别
    python 部分函数
    uiautomatorviewer错误 unable toconnect to adb
    pyqt 不规则形状窗口显示
    appium 计算器demo
    Spring 3.0 注解注入详解
    Spring Autowire自动装配
    restful 学习地址
  • 原文地址:https://www.cnblogs.com/HotPants/p/13965473.html
Copyright © 2011-2022 走看看