CSP+NOIP总结

CSP

普及:
自认为这次考的是很不理想的,因为平时的习惯,连桶排这种基础算法都没有掌握,排序到考前为止只会sort,这就导致了与省一无缘
T3直接就放弃了,当时只想着T4如果能打出来就有省一了
且T4也分析错误了,没有想到分类来讨论从上往下走和从下往上走的情况,以至于只打了爆搜,10tps
如果考前能够再老老实实复习一遍基本的排序和DP,也许结果就大不一样了
这次也是给自己敲响了警钟,因为如果还不能调整好状态的话跟别人的差距可能就越来越大了


提高:
T1直接放弃
然后再打T2和T3的暴力
事实证明以上策略是很有效的
T4如果不那么贪心,直接讨论n=3恐怕还能再多得20tps
感觉自己离提高组的差距还是太大了,没有掌握的东西还有很多


题解部分:
普及:

1.优秀的拆分

本来是一道二进制的题,但考场上是用快速幂做的
有个坑点是必须从大到小枚举,因为如果从小到大再从大到小可能凑不出来n
且n为奇时肯定凑不出来,因为必须用到2的0次方,不符合要求
code:

#include
#include
using namespace std;
int ksm(int a,int b)
{
    int ans=1;
    while(b)
    {
        if(b&1)
        {
            ans=ans*a;
        }
        a=a*a;
        b>>=1;
    }
    return ans;
}
int main()
{
    int n;
    scanf("%d",&n);
    if(n%2!=0)
    {
        printf("-1");
        return 0;
    }
    while(n)
    {
        for(int i=24;i>=1;i--)//1e7<2^24
        {
            if(ksm(2,i)<=n){
                printf("%d ",ksm(2,i));
                n-=ksm(2,i);
                break;
            }
        }
    }
    return 0;
}

2.直播获奖

本题目的考点在于需要实时进行排序,而sort的时间复杂度为n,显然n^2的时间复杂度是过不了这道题的
那么我们观察到w[i]<=600,也就是说我们可以把每个w[i]放到一个数组a里面,a[w[i]]的个数就是当前得分为w[i]的人数,这时我们从600往1查找,如果当前总人数>=max( i ∗ w i*w iw%,1),就代表已经到了排名为max( i ∗ w i*w iw%,1)的值,输出即可
时间复杂度为O(600*n)
code:

#include
#include
#include
using namespace std;
int n,a[1000005],w,t[605];
int main()
{
    scanf("%d%d",&n,&w);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        int p=max(1,i*w/100),now=0;//now为当前总人数
        t[a[i]]++;
        for(int j=600;i>=0;j--){
            now+=t[j];
            if(now>=p)
            {
                printf("%d ",j);
                break;
            }
        }
    }
    return 0;
}

3.表达式

为了解决这道题,我们需要引入一个知识点:表达式树
通俗来说,其实就是通过栈来将后缀表达式的数字和符号建一颗子节点为数字或字符,父节点为字符的树
样例一的表达式树就如下图
CSP+NOIP总结_第1张图片
而一颗子树的值,就是当前子树的左子树的值和当前子树的右子树的值进行当前子树根节点的操作后的值
又因为&运算结果为1时,必须两个都为1,那么只要有当前子树的一颗子树的取值为0,另一颗不论取何值,都无法对当前子树的结果产生影响
又因为&运算结果为1时,只要一个为1即可,那么只要有当前子树的一颗子树的取值为1,另一颗不论取何值,都无法对当前子树的结果产生影响
即我们可以用一个数组is来表示当前结点的值取反后是否会对所在当前结点父亲为根节点的子树的值造成影响
其中is[x]=1表示没有影响,is[x]=0表示有影响
另一点需要注意的是,如果当前结点的父节点取反对当前子树的取值没有影响,那么无论当前结点取反是否会影响到以当前结点的子树的值,都不会对当前子树的取值产生影响
换句话说,我们可以通过下传每一个父亲节点对当前子树有无影响从而判断取反该节点有无影响
例如当前节点的is为0,而其父亲结点的is为1
那么我们将当前节点的is|父亲节点的is,这个节点的is就变成了1
而如果其父亲节点的is为0,那么说明父亲节点的取反肯定会对当前子树造成影响,那么它的取反既然也能对当前子树产生影响,那么肯定取反他会影响当前子树
又因为每次取反的元素只有一个,肯定不可能出现它与它父亲同时取反的情况,那么当前点的is如果为0,肯定对最终的答案会造成影响
即,当前节点取反对父亲造成影响,父亲取反又对父亲的父亲造成影响。。。
那么最终如果根节点为0的话一定会对根节点产生影响(否则标记下传时有一个父亲节点为1,那么肯定不会改变)
所以这个递归就从根节点|0开始(|1的话会导致所有结点都不会产生影响)
code:

#include
#include
#include
using namespace std;
char S[1000005];
int n,od[1000005];//od为当前树上结点的操作符,-1表示&,-2表示|,否则表示未知数
int tree[1000005],s[1000005],top,cnt;//tree:每个未知数在树上的结点编号 s:手写栈
int l[1000005],r[1000005],a[1000005];//l:左儿子 r:右儿子 a:未知数的值
bool val[1000005],is[1000005],change[1000005];//val:当前节点为根的子树的值,change:当前节点的值是否取反
void xf(int now,int v){//下放
	if(now==-1)
	{
		return;
	}
	is[now]|=v;//父结点is下传
	xf(l[now],is[now]);
	xf(r[now],is[now]);
}
void dfs(int now)
{
	if(now==-1){
		return;
	}
	dfs(l[now]);
	dfs(r[now]);//递归左右儿子
	if(od[now]>0)//如果当前结点为数字
	{
		val[now]=a[od[now]];//这时od存的是当前未知数的下标
		if(change[now])//如果要取反
		{
			val[now]=!val[now];
		}
	}
	else{
		if(od[now]==-1)//如果为&操作
		{
			val[now]=val[l[now]]&val[r[now]];
			if(change[now])
			{
				val[now]=!val[now];
			}
			if(val[l[now]]==0)
			{
				is[r[now]]=1;
			}
			if(val[r[now]]==0)
			{
				is[l[now]]=1;
			}
		}
		if(od[now]==-2)//否则是|操作
		{
			val[now]=val[l[now]]|val[r[now]];
			if(change[now])
			{
				val[now]=!val[now];
			}
			if(val[l[now]]==1)
			{
				is[r[now]]=1;
			}
			if(val[r[now]]==1)
			{
				is[l[now]]=1;
			}
		}
	}
}
int main()
{
	memset(l,-1,sizeof(l));
	memset(r,-1,sizeof(r));
	gets(S);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	int q;
	int len=strlen(S);
	for(int i=0;i<len;)
	{
		char c=S[i];
		if(c=='x')
		{
			int ans=0;
			i++;
			while(S[i]>='0'&&S[i]<='9')
			{
				ans=ans*10+S[i++]-'0';
			}
			i--;
			s[++top]=++cnt;
			od[cnt]=ans;
			tree[ans]=cnt;//当前节点树上编号为cnt
		}
		else if(c=='&')
		{
			int X1=s[top--],X2=s[top--];//以当前符号为根节点建树
			s[++top]=++cnt;//因为栈里保存的是编号,值的记录要到表达式树里去操作
			od[cnt]=-1;
			l[cnt]=X1,r[cnt]=X2;
		}
		else if(c=='|')
		{
			int X1=s[top--],X2=s[top--];
			s[++top]=++cnt;
			od[cnt]=-2;
			l[cnt]=X1,r[cnt]=X2;
		}
		else if(c=='!')
			 {
				int X1=s[top];
				change[X1]=!change[X1];
			 }
		i++;
	}
	int root=s[top];//根节点为最后执行的操作
	dfs(root);
	bool K=val[root];//求出原始值
	xf(root,0);//下放影响标记
	scanf("%d",&q);
	while(q--){
		int x;
		scanf("%d",&x);
		if(is[tree[x]])//如果当前节点会对根节点有影响
		{
			printf("%d\n",K);
		}
		else{
			printf("%d\n",!K);
		}
	}
	return 0;
}

4.方格取数

因为只能往 右,上,下三个方向走,又因为终点在(n,m),每一列的答案都由上一列转移过来
而只有三种方法转移值
1.由a[i][j-1]+a[i][j]
2.从上方某一个点的j-1一直往下走到a[i][j]
3.从下方某一个点的j-1一直往上走到a[i][j]
即如果不是一直走到底,那么就要重新从上一列的值走起
如果重新走值更大,那么肯定给当前值的价值更大,对它下面(或上面)的元素更有利,因为必定有一种情况是最优解
三者取max即可
第一列的值就是从a[1][1]到a[n][1]的前缀和

#include
#include
#include
using namespace std;
#define ll long long
int n,m;
ll a[1005][1005],dp[1005][1005];
int main()
{
	memset(dp,-0x3f,sizeof(dp));
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%lld",&a[i][j]);
		}
	}
	dp[1][1]=a[1][1];
	for(int i=2;i<=n;i++)
	{
		dp[i][1]=dp[i-1][1]+a[i][1];
	}
	for(int j=2;j<=m;j++)
	{
		ll now=-1e17;
		for(int i=1;i<=n;i++)
		{
			now=max(now,dp[i][j-1]);//如果now被dp[i][j-1]更新,情况1
			now+=a[i][j];//否则,情况2
			dp[i][j]=max(dp[i][j],now);
		}
		now=-1e17;
		for(int i=n;i>=1;i--)
		{
			now=max(now,dp[i][j-1]);//如果now被dp[i][j-1]更新,情况1
			now+=a[i][j];//否则,情况3
			dp[i][j]=max(dp[i][j],now);
		}
	}
	printf("%lld",dp[n][m]);
	return 0;
}

提高:

1.动物园

其实我们可以把所有现有的动物园里的动物全部或起来,这样每种动物所需要饲料在最终的这头动物上都有
如果最终动物&上2^p[i]不为最终动物,即最终动物第p[i]位为0,那么这种饲料的动物全部不能满足
也就是二进制第p[i]为为1的数字都不能取
观察到动物总个数就会少一半
所以k–,当前p[i]已找过,所以vis[p[i]]=1
最后输出pow(2,k)即可
k=64,n=0的特殊情况会超ull,特判即可
code:

#include
#include
#include
using namespace std;
unsigned long long n,m,c,k,sum,ans;
bool vis[1000005];
int main()
{
	scanf("%llu%llu%llu%llu",&n,&m,&c,&k);
	for(unsigned long long i=1;i<=n;i++)
	{
		unsigned long long p;
		scanf("%llu",&p);
		sum|=p;
	}
	for(unsigned long long i=1;i<=m;i++)
	{
		unsigned long long p,q;
		scanf("%llu%llu",&p,&q);
		if((sum|(1ull<<p))!=sum&&!vis[p])
		{
			k--;
			vis[p]=true;
		}
	}
	if(k==64&&n==0)
	{
		printf("18446744073709551616");
		return 0;
	}
	printf("%llu",(unsigned long long)pow(2,k)-n);//n种动物已有,所以减去
	return 0;
}

NOIP

三年一次字符串,今年就考到了
T1就是一道拓扑排序,但是需要高精
因为一个蓄水点想要排水,必须要等到其被每个父亲节点的水排进后才能排出,否则会重复计算之前排出的水
又因为管道无环,所以肯定不存在子节点未等父节点排完向父节点排水
考试时先用了一个Cnt数组来保存的出现个数,后来才想起就是一道拓扑排序
T2因为不知道如何判断奇数次字符个数所以没有打,hash和KMP都想到了的
T3不会
T4暴力模拟
T1不加高精code:

#include
#include
#include
using namespace std;
int n,m,P[1000005],cnt,vis[1000005],Cnt[1000005];
struct water{
	long long out,in;
	vector<int> v;
}a[1000005];
long long Gcd(long long a,long long b){
	return b==0?a:Gcd(b,a%b);
}
long long Lcm(long long a,long long b)
{
	return a*b/(Gcd(a,b));
}
void dfs(int now,long long In,long long Out)
{
	if(a[now].in==0)
	{
		a[now].in=In;
		a[now].out=Out;
	}
	else{
		long long K=Lcm(a[now].out,Out);
		a[now].in=a[now].in*(K/a[now].out);
		a[now].in+=In*(K/Out);
		a[now].out=K;
	}
	long long O=Gcd(a[now].in,a[now].out);
	a[now].in/=O;
	a[now].out/=O;
	vis[now]++;
	if(vis[now]==Cnt[now])
	{
		for(int i=0;i<a[now].v.size();i++)
		{
			dfs(a[now].v[i],a[now].in,a[now].out*a[now].v.size());
		}
	}
}
int main()
{
	//freopen("water.in","r",stdin);
	//freopen("water.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		int p,x;
		scanf("%d",&p);
		if(p==0)
		{
			P[++cnt]=i;
		}
		else{
			for(int j=1;j<=p;j++)
			{
				scanf("%d",&x);
				Cnt[x]++;
				a[i].v.push_back(x);
			}
		}
	}
	for(int i=1;i<=m;i++)
	{
		Cnt[i]=1;
		dfs(i,a[i].v.size(),a[i].v.size());
	}
	for(int i=1;i<=cnt;i++)
	{
		long long k=Gcd(a[P[i]].in,a[P[i]].out);
		printf("%lld %lld\n",a[P[i]].in/k,a[P[i]].out/k);
	}
	return 0;
}

你可能感兴趣的:(总结)