2022年上海5月月赛乙组题解

IAI上海月赛系列文章目录

T1 天平砝码V2

在这里插入图片描述
在这里插入图片描述

思路一 暴力枚举法O(3^n)

#include<stdio.h> 
int n;
int res;
int w[1000000]; 
bool st[100000];
void dfs(int k,int sum)//表示k个的砝码,重量是sum 
{
	if(k>n)//k>n 说明选完n个砝码 
	{
		if(sum>0&&!st[sum])// 判断选出来的n个砝码的重量是否没被标记过 ,如没标记则答案加1 
		{
			res++;
			st[sum]=true;//标记这个重量 
			return;
		}	
	}
	//还没选够n个砝码 
	else
	{
		dfs(k+1,sum-w[k]);//砝码放右边 
		dfs(k+1,sum);//跳过,不适用当前的砝码 
		dfs(k+1,sum+w[k]);//砝码放左边 
	}
}
int main()
{
	scanf("%d",&n); 
	for(int i=1;i<=n;i++)scanf("%d",&w[i]); 
	
	dfs(0,0);
	printf("%d",res); 
}

思路二 动态规划dp

进入第i个砝码时候可以,我们需要先更新dp(i, ai) = 1;

当dp(i-1,j)=1时,第i个砝码重量为ai,

状态转移如下:

  1. dp (i , j) = 1,第i个砝码不使用

  2. dp (i,j+ai)=1,第i个砝码和之前能称出的重量j相加

  3. dp(i,j-ai)=1,两相减,注意可能小于0.需要加绝对值。

  4. dp(i,ai-j)=1,两相减,注意可能小于0.需要加绝对值。

普通版本(内存未优化)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
bool dp[105][100005];	//dp[i][j]=1表示i个砝码可以称出重量j 
int a[105];
int sum;
int main() 
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		sum+=a[i];	//计算出可以称出的最大重量 
	}
	dp[1][a[1]]=1; 
	for(int i=2;i<=n;i++)	//枚举每个砝码 (加入每个砝码) 
	{	
	    dp[i][a[i]]=1;	//当前砝码的重量一定可以称出来
		for(int j=1;j<=sum;j++)	//复制状态,加入当前砝码前可以称得的重量加入后也可以得到 
			if(dp[i-1][j])//如果i-1个砝码可以称出j,那么枚举加入新的砝码可以称得的重量
            {
               	dp[i][j]=dp[i-1][j]; 
                dp[i][j+a[i]]=1;
				dp[i][abs(j-a[i])]=1; 
            }
	}
	int ans=0;
	for(int j=1;j<=sum;j++)
	{
		if(dp[n][j]!=0)
			ans++;
	}
	cout<<ans<<endl;
	return 0;
}
内存优化版本

实际上只要用到dp(i-1) 和dp(i) 两个状态。因此我们可以把空间做一次优化。

#include<iostream>
#include<algorithm>
using namespace std;

//bool dp[107][100005];//表示前i个砝码,可以表示的重量j
bool dp[100005],f[100005];//空间优化
int a[107];
int sum;
int main()
{
    freopen("11.txt", "r", stdin) ;
    int n;
    scanf("%d", &n);

    for (int i = 1;i<=n;i++)
    {
        cin >> a[i];
        sum += a[i];
    }
        
    
    for (int i = 1;i<=n;i++)
    {
        //把dp中的值给f
        for (int i = 0;i < sum;i++)
            f[i] = dp[i];
        for (int j = 1; j <= sum; j++)
        {
           if (f[j])
            {
                dp[abs(j - a[i])] = 1;
                dp[j + a[i]] = 1;
            }
        }
        dp[a[i]] = 1;
    }
    int res = 0;
    for (int i = 1;i<100005;i++)
         if(dp[i])res++;
    cout << res << endl;
    return 0;
}
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

//bool dp[107][100005];//表示前i个砝码,可以表示的重量j
bool dp[2][100005];//空间优化
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1;i<=n;i++)
    {
        int ai;
        scanf("%d", &ai);
        memcpy(dp[0],dp[1],sizeof dp[1]);
        / 更新dp[1]状态。// 
        for(int j=0; j<100005; j++)
        {
            if(dp[0][j]) 
            {
                dp[1][j+ai] = 1;
                dp[1][abs(j-ai)] =1;
                // dp[1][j]=1;
            }
        }
        dp[1][ai]=1;
        
    }
    int res = 0;
    for (int i = 1;i<100005;i++)
         if(dp[1][i])res++;
    cout << res << endl;
    return 0;
}

T2 数山峰(二)

题目描述

在平面直角坐标系上有 n座像山峰一样的图案。每座山峰是一个直角等腰三角形,它们的底边都是坐标系的X轴,它们的峰顶在第一象限里,其中第 i座山峰的峰顶坐标为 (x_i,y_i)。

如果一座山峰的一部分在另一座山峰的内部,那么这两部分山峰就重叠了。给定每个山峰的峰顶坐标,请统计不重复计算重叠部分的前提下,这些山峰的总面积是多少。

输入格式

第一行:单个整数 n
第二行到第 n+1 行:第 i+1行两个整数,表示一个峰顶的坐标 x_i与 y_i。

输出格式

单个整数:设所有的山峰的可见面积为 ss,因为希望输出整数,所以规定输出 4s4s

数据范围
  • 对于 30% 的数据,1≤n≤1,000;
  • 对于 60% 的数据,1≤n≤10,000;
  • 对于100% 的数据,1≤n≤100,000;
  • 1≤xi,yi≤100,000,000。

思路一

思路:
1、依据题意,可以考虑先将三角形按X轴底边左端点升序(左端点相同按右端点升序)结构体数组a记录三角形底边的左右端点lx、rx,pre记录前一个累加到面积值sum的三角形。
2、再分析下图:
在这里插入图片描述

顺序枚举三角形i
(1)如它与之前的pre三角形不重合(如:大的黑色三角形pre与当前黄色三角形i),即:pre.rx<=a[i].lx 则更新 pre为当前三角形i、累加三角形i的面积 * 4到sum
易知:三角形i的面积 * 4=(a[i].rx-a[i].lx) * (a[i].rx-a[i].lx)/2/2 *4=(a[i].rx-a[i].lx)^2
(2)否则:重合有两种情形
①被包含(如小的红色三角形pre和当前阴影小三角形i),即:a[i].rx<=pre.rx 则不更新,继续枚举
②部分交叉(如小的红色三角形pre和当前大的红色三角形i),即:a[i].rx>pre.rx
则更新sum+=(a[i].rx-a[i].lx)2-(pre.rx-a[i].lx)2 pre为当前三角形 i
注意:类似大的红色三角形pre与当前黑色三角形i也符合以上部分交叉条件。

3、如此,时间复杂度为O(nlgn)。

Code1
#include<iostream>
#include<cstdio>
#include<algorithm> 
using namespace std;
/*思路:根据三角形与x轴的交点(左右端点),按左端点升序排序,考虑:枚举考虑三角形i,
(1)与之前三角形不重叠即:a[i].lx>=pre.rx,更新pre=a[i] sum+=(a[i].rx-a[i].lx)^2 
(2)重叠:
①若被包含即:pre.lx<=a[i].lx 且 a[i].rx<=pre.rx  不更新 
②若交叉即:pre.lx<=a[i].lx 且 a[i].rx>pre.rx 更新sum+=((a[i].rx-a[i].lx)^2-(pre.rx-a[i].lx)^2) pre=a[i] 
*/
const int N=1e5+5;
typedef long long ll;
struct Node{
	int lx,rx;
}a[N],pre;
int n;
ll sum;
int cmp(Node x,Node y){//左端点升序,相同按右端点升序 
	if((x.lx<y.lx)||(x.lx==y.lx&&x.rx<y.rx)) return 1;
	return 0;
}
ll calc(ll x){
	return x*x;
}
int main(){
	//freopen("count.in","r",stdin);
	//freopen("count.out","w",stdout);
	ios::sync_with_stdio(false);
	cin>>n;
	for(int i=1;i<=n;i++){
		int x,y;
		cin>>x>>y;
		a[i].lx=x-y, a[i].rx=x+y;//三角形i与x轴相交的左右端点
	}
	sort(a+1,a+n+1,cmp);
	//for(int i=1;i<=n;i++)
	//	printf("(%d,%d)%c",a[i].lx,a[i].rx,i==n?'\n':' ');
	pre=a[1], sum+=calc(a[1].rx-a[1].lx);
	for(int i=2;i<=n;i++){
		if(pre.rx<=a[i].lx) //不重叠 更新
			pre=a[i], sum+=calc(a[i].rx-a[i].lx);
		//if(pre.lx<a[i].lx&&a[i].rx>pre.rx)//有交叉 更新 
		//	sum+=calc(a[i].rx-a[i].lx)-calc(pre.rx-a[i].lx), pre=a[i];
		else{
			if(a[i].rx<=pre.rx) continue;//被包含 不更新 
			else  sum+=calc(a[i].rx-a[i].lx)-calc(pre.rx-a[i].lx), pre=a[i];//有交叉 更新 
		}
	}
	cout<<sum<<endl; 
	return 0;
}

思路二

每个三角形,左直边满足y-x=b,右直边满足x+y=a;按照右直边优先,左直边其次,从上到下(即降序)排列!
按照排好的顺序,遍历每个三角形,h代表当前左直边最高的三角形编号,如遇到第i个三角形左直边更高,计算第i三角形右直边与第h三角形左直边交点的y坐标,更新总面积,更新h,直至遍历结束,得解!
备注:计算面积是直接扩大4倍,更新面积时候需要减去重叠部分,当交点位于x轴或第3、4象限时无重叠!

Code2
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1000005;
int n;
struct Node{
	ll x,y,a,b;
}sjx[N];
bool cmp1(Node xx,Node yy){//按照右直边a优先,左直边b其次,降序排列
	if(xx.a==yy.a)return xx.b>yy.b;
	return xx.a>yy.a;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>sjx[i].x>>sjx[i].y;
		sjx[i].a=sjx[i].x+sjx[i].y;
		sjx[i].b=sjx[i].y-sjx[i].x;
	}
	sort(sjx+1,sjx+n+1,cmp1);
	ll s=sjx[1].y*sjx[1].y*4;//面积统一扩大四倍
	int i=2,h=1;
	while(i<=n){
		while((sjx[i].b<=sjx[h].b)&&i<=n)i++;
		if(i>n)break;
		ll yy=sjx[i].a+sjx[h].b;//找到交点
		if(yy<0)yy=0;//如果交点在x轴上,或交点在第三、四象限
		s+=sjx[i].y*sjx[i].y*4-yy*yy;//计算增加面积,要减去重叠部分面积
		h=i;i++;
	}
	cout<<s<<endl;
	return 0;
}

思路三

本题是一道数学思维题,容易想到一种做法是计算出所有的面积之后减去重复的部分。
对于全覆盖来说,直接减去被覆盖的即可。y*y就是它的面积。

对于覆盖部分的,需要利用坐标去计算,假如两个三角形x_1,y_1x1,y1,x_2,y_2x2,y2覆盖部分,它们的面积就是需要首先推导出各个三角形底边的左端点坐标和右端点左边,然后根据这个坐标差可以计算出三角形的底,由于等腰直角三角形的缘故,高等于这个底的一半。因此可以计算出部分覆盖的面积。

但是当你处理发现的时候并不是那么容易处理,思考以下这组数据。
6
1 1
2 2
3 3
8 6
16 16
32 3
如果我们一个个的看过去,会发现计算起来不顺利,因为最后的一个16 16这个三角形居然覆盖了前面所有的,那么导致前面的三角形其实都是多余的。因此我们可以先对这些输入的三角形去重,将所有全覆盖的去重只留下最大的那个三角形,去重后只剩下了部分覆盖的,我们一个个看过去去计算就不会错了。

所以我们首先可以对所有的坐标排序,排序按照左端优先,左端相同按照高度最大的在前面。

Code3
#include <bits/stdc++.h>
using namespace std;
#define N 100005
struct node
{
	long long x, y, l, r;
} a[N];
bool cmp(node c, node d)
{
	if (c.l != d.l)
		return c.l < d.l;
	return c.y > d.y;
}
vector<node> v, e;
int main()
{
	int n;
	long long sum = 0;
	long long ans = 0;
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i].x >> a[i].y;
		a[i].l = a[i].x - a[i].y;
		a[i].r = a[i].x + a[i].y;
        //计算左右端点
	}
	sort(a + 1, a + n + 1, cmp);
	v.push_back(a[1]);//接下来这个循环就是去重
	for (int i = 2; i <= n; i++)
	{
		if (a[i].l == a[i - 1].l)
		{
			continue;
		}
		else
		{
			v.push_back(a[i]);
		}
	}
	e.push_back(v[0]);//这里的去重只是去除了左端点一样的全覆盖
	long long tmpx = v[0].l, tmpy = v[0].r;
	for (int i = 1; i < v.size(); i++)//再次去重去掉,
	//内部覆盖的三角形。
	{
		if (v[i].l >= tmpx && v[i].r <= tmpy)
			continue;
		else
		{
			e.push_back(v[i]);
			tmpx = v[i].l, tmpy = v[i].r;
		}
	}
    //经过上述两次去重就没有全覆盖了
	for (int i = 0; i < e.size(); i++)//计算留下的所有面积
	{
		sum += e[i].y * e[i].y;
	}
	sum*=4;
	long long q = 0;
	for (int i = 1; i < e.size(); i++)
	{
		if (e[i - 1].r > e[i].l)//只计算半覆盖的重复面积
		{
			q += (e[i - 1].r - e[i].l) * (e[i - 1].r - e[i].l);//由于又要除以4
			//又要乘以4,避免精度丢失,因此省去这一步。
		}
	}
	sum -= q;
	ans = sum ;
	cout << ans;
}

T3 狼人游戏(二)

题目描述

有 n名玩家在玩狼人游戏,有一些玩家的身份是狼人。其余玩家的身份是预言家。游戏的进程中,陆续出现了 m句发言,每句发言来自于某个玩家,发言的信息是声称另一个玩家的身份是狼人或者是预言家。

小爱猜想,狼人的发言应该永远与事实相反,而预言家的发言应该永远与事实相同。她想检查一下,她的猜想是否会与发言记录产生矛盾,如果出现矛盾,请求出她的猜想与哪一条发言最先出现矛盾。

输入格式

第一行:两个整数 n与 m
第二行到 m+1行:第 i+1行有两个整数:s_i和 o_i,接下来有一个字符:
– 如果是 T,表示 s_i宣称 o_i是预言家;
– 如果是 F,表示 s_i宣称 o_i 是狼人;

输出格式

如果没有矛盾,输出 Passed,否则输出第一次出现矛盾的位置。

思路一:

可以用并查集


状态定义

我们将第i+MAX个人作为第i个人的影子
(即状态不同,一预一狼,不在同一集合)

发言的作用

通过分析,知道

  • T代表s,o是同类
    因此,我们合并s与o
    以及s的影子与o的影子
  • F代表s,o是异类
    因此,我们合并s与o的影子
    以及s的影子与o
矛盾判断

已经说过影子与自身状态不同,不在同一集合
如果在同一集合就矛盾了


注意:
数组记得开MAX**2*

Code

#include<bits/stdc++.h>
using namespace std;

#define MAX 500500

// 并查集模板
struct UF {
    UF* fa;

    UF() {fa=this;}
    UF* father() {
        if(fa!=this) fa=fa->father();
        return fa;
    }
    bool with(UF& b) {return father()==b.father();}
    void add(UF& b) {if(!with(b)) father()->fa=b.father();}
}uf[MAX*2];

// 变量定义
int n,m,s,o;
char c;

// 返回0为成功,否则为矛盾出现的地方
int test() {
    // 输入nm
    cin>>n>>m;
    for(int i=1;i<=m;i++) {
        // 输入每一条
        cin>>s>>o>>c;
        if(c=='T') {
            // s,o同类
            uf[s].add(uf[o]);
            uf[MAX+s].add(uf[MAX+o]);
        }else if(c=='F') {
            // s,o异类
            uf[MAX+s].add(uf[o]);
            uf[s].add(uf[MAX+o]);
        }

        // 矛盾判断
        if(uf[s].with(uf[MAX+s])) return i;
    }
    return 0;
}

signed main() {
    int ans=test();
    if(ans) cout<<ans<<endl;
    else cout<<"Passed"<<endl;

    return 0;
}

思路二:

前置知识:并查集,

本题不断地再说一个人是预言家一个人是狼人,并且问你何时会有矛盾。自然可以想到用并查集维护两个集合,一个狼人集合,一个预言家集合。这就是本关的知识点,种类并查集,类似的题目也有很多。noip的关押罪犯,noi的食物链等。

因为维护两个集合,因此fa数组开到2n,1到n1到n记录预言家,n+1到2n记录狼人集合。

当输入是T,有两种情况。

  1. u是预言家,根据猜测,预言家说真话,因此v也是预言家,则此时我们需要合并u,v,u+n和v+n。也就是合并两个预言家到集合里,u+n和v+n对应的就是它们的狼人也合并进去。
  2. u是狼人,根据猜测,狼人说假话,因此v不是预言家也是狼人。所以也是合并u,vu,v,u+nu+n和v+nv+n。也就是合并两个预言家到集合里,u+nu+n和v+nv+n对应的狼人也合并进去。

什么时候冲突?

我们合并的是u,v,u+n和v+n

自然是当find(u) == find(v + n) || find(u + n) == find(v)

翻译过来具体的含义就是u这个预言家和v+n这个狼人出现在一个集合,u+n这个狼人和v这个预言家出现同一个集合。

当输入是F,分析方式同上。

#include <bits/stdc++.h>
using namespace std;
const int N = 1000005;
int n, m, fa[N], u, v;
char opt;
int find(int x)//查找祖先
{
	if (x != fa[x])
	{
		fa[x] = find(fa[x]);
	}
	return fa[x];
}
void join(int x, int y)//合并两个集合
{
	x = find(x), y = find(y);
	fa[x] = y;
	return;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n >> m;
	for (int i = 1; i <= n * 2; i++)//初始化
		fa[i] = i;
	for (int i = 1; i <= m; i++)
	{
		cin >> u >> v >> opt;
		if (opt == 'T')
		{
			if (find(u) == find(v + n) || find(u + n) == find(v))
			{
				cout << i;
				return 0;
			}
			join(u, v);
			join(u + n, v + n);
		}
		if (opt == 'F')
		{
			if (find(u + n) == find(v + n) || find(u) == find(v))
			{
				cout << i;
				return 0;
			}
			join(u + n, v);
			join(v + n, u);
		}
	}
	cout << "Passed";
	return 0;
}

思路三

这题是一个权值并查集裸题。

我们维护一个 并查集 A记录每个节点的父亲节点,顺带用一个数组 Q记录上每个节点对应的玩家和父亲节点对应玩家是否是同伙(都为预言家、都为狼人),是则为 0,不是则是 1。

很容易知道,如果 s说 o是好人,他们要么双人,要么双狼,是同类,否则就是一人一狼,不同类。假如说 s宣称 o与自己的关系为 f(同类为 0,异类为 1),那么就将 s和 o和各自的根节点路径压缩,让 s和 o 直接连向根节点:

  • 如果两个根节点相同,那么 如下,否则就可以推出矛盾,直接输出+退出即可。

q s  xor  q o  xor  f = 0 q_s\ \text{xor}\ q_o\ \text{xor}\ f=0 qs xor qo xor f=0

  • 否则设这两个根节点分别为 fsf**s 和 fof**o,那么就让 fsf**s 连向 fof**o,然后 :=q**s xor q**o xor f 即可。

q f s : = q s  xor  q o  xor  f q_{fs}:=q_s\ \text{xor}\ q_o\ \text{xor}\ f qfs:=qs xor qo xor f

直到最后没有矛盾,输出 Passed 即可。时间复杂度不多说。

#include<bits/stdc++.h>
#define maxn 500005
#define F(i,b) for(int i=1;i<=b;i++)
using namespace std;

int n,m,a[maxn],q[maxn];

int A(int u){
	if(a[u]==u)return u;
	int ret=A(a[u]);
	q[u]^=q[a[u]];
	return a[u]=ret;
}

int main(){
	cin>>n>>m;
	F(i,n)a[i]=i;
	F(i,m){
		int s,o,f=0,fs,fo;
		char c;
		cin>>s>>o>>c;
		if(c=='F')f=1;
		fs=A(s);fo=A(o);
		f^=q[s]^q[o];
		if(fs==fo){
			if(f){
				cout<<i;
				return 0;
			}
			else continue;
		}
		a[fs]=fo;q[fs]=f;
	}
	cout<<"Passed";
	return 0;
}

T4 平衡三进制

     -25.4,圆整#为-25;       ‡                       余,-0.4;♦
   -25÷3=-8⅓, 圆整为- 8;余,-1↑; ‡ -0.4×3=-1.2, 圆整为-1|;余,-0.2;
   - 8÷3=-2⅔, 圆整为- 3;余, 1|; ‡ -0.2×3=-0.6, 圆整为-1|;余, 0.4;
   - 3÷3=-1 , 圆整为- 1;余, 0|;0.4×3= 1.2, 圆整为 1|;余, 0.2;
   - 1÷3=- ⅓, 圆整为  0;余,-1|;0.2×3= 0.6, 圆整为 1;余,-0.4;跳入循环
       -25.410=T01T.TT113

思路一

#include<bits/stdc++.h> 
using namespace std;
#define ll long long

int t[100],cnt = 0; // t存放普通的三进制数
char c[100]; // 存放平衡三进制数

void solve(int n)
{
    int m = abs(n); // 取n 的绝对值
    while(m)// 求出m的三进制。
    {
        t[cnt++] = m%3;
        m/=3;
    }
    for (int i = 0; i<= cnt; i++ )
    {
        if(t[i] == 2) 
        {
            t[i+1] +=1;
            c[i] = 'z';
        }
        else if(t[i] == 1)c[i]='1';
        else if(t[i] == 0)c[i]='0';
        else if(t[i] == 3)
        {
            c[i] = '0';
            t[i+1] += 1;
        }
    }
    if(n<0)// n < 0 时需要做一次处理,即把1的地方变为z 把z 的地方变为1. 
    {
        for (int i = 0; i <= cnt; i ++ )
        {
            if(c[i] == '1') c[i] = 'z';
            else if(c[i] =='z') c[i] = '1';
        }
    }
}

int main()
{
    int n;
    cin >> n;
    solve(n);
    // for (int i = cnt; i >= 0; i -- ) cout<< t[i];
    // cout <<endl;
    if(n == 0) 
    {
        cout<< 0; 
        return 0;
    }
    for (int i = cnt; i >= 0; i -- )
    {
        if(i == cnt && c[i] !='0') cout<< c[i];
        else if(i!=cnt) cout<<c[i];
    }
    return 0;
}

思路二

递归解法

#include <iostream>
using namespace std;
void f(int x) {
	if (x == 0) return;
	
	if (x%3 == 2) f(x/3 + 1), cout << 'z';
	else if (x%3 == -2) f(x/3 - 1), cout << '1';
	else if (x%3 == -1) f(x/3), cout << 'z';
	else f(x/3), cout << x%3;
}

int main() {
	int n;
	cin >> n;

	if (n == 0) cout << 0;
	else f(n);

	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值