2021上海省赛 比赛链接:点击这里传送
A 题目链接:点击这里传送
思路:
输入两个向量,求出他们的叉乘。
#include<bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int x,y,z,xx,yy,zz;
cin>>x>>y>>z;
cin>>xx>>yy>>zz;
int a=y*zz-yy*z;
int b=xx*z-x*zz;
int c=x*yy-xx*y;
cout<<a<<" "<<b<<" "<<c<<endl;
return 0;
}
B 题目链接:点击这里传送
思路:
显然
O
(
n
3
)
O(n^3)
O(n3)的dp是不行的。
如果只考虑其中两种牌呢?
如果只有两类牌a和b,那么我们可以根据b-a的值进行倒序排序,这样就可以进行贪心(优先选b)
接着考虑对另一类牌c进行排序。
定义
d
p
[
i
]
[
j
]
dp\left[i\right]\left[j\right]
dp[i][j]为总共选了i张牌,c类牌选了j张的情况(
j
<
=
i
j<=i
j<=i)
这样就分成了两种情况:
- 选了c类牌 状态转移方程: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − 1 ] + c a r d [ i ] . c , d p [ i ] [ j ] ) dp[i][j]=max(dp[i-1][j-1]+card[i].c,dp[i][j]) dp[i][j]=max(dp[i−1][j−1]+card[i].c,dp[i][j])
- 没选c类牌 此时选了谁的结果由之前贪心定义的排序顺序决定谁的优先级高 状态转移方程: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] + ? ? ? , d p [ i ] [ j ] ) dp[i][j]=max(dp[i-1][j]+???,dp[i][j]) dp[i][j]=max(dp[i−1][j]+???,dp[i][j])
通过这样的方法,可以将复杂度降为
O
(
n
2
)
O(n^2)
O(n2)
需要特别注意的是,对于
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]中
j
>
i
j>i
j>i的部分是非法情况。而当i=j时,
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]会访问到非法情况,所以要对这些非法情况初始化为无穷小(理论上,这题是-1e9)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 5005
ll dp[MAXN][MAXN];//表示为前i次选择选了j次第三类,选了i-j次第一类和第二类
struct node
{
ll a;
ll b;
ll c;
}card[MAXN];
ll n,a,b,c;
bool cmp(node a,node b)//排序后优先选b,因为b大的都被移到了前面
{
if(a.b-a.a==b.b-b.a) return a.c>b.c;
return a.b-a.a>b.b-b.a;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>a>>b>>c;
for(int i=1;i<=n;i++)
{
cin>>card[i].a>>card[i].b>>card[i].c;
}
for(int i=0;i<=n;i++)//dp[i-1][j] 会导致i<j的情况,必须初始化为无穷小(让它够小到无法更新dp[i][j]的值,因为这种i<j的情况是非法的,不可能存在的)
{
for(int j=i+1;j<=c;j++)
{
dp[i][j]=-1e9;
}
}
sort(card+1,card+1+n,cmp);
for(int i=1;i<=n;i++)//第i个物品
{
for(int j=0;j<=min(i,(int)c);j++)//选了j个第三类物品
{
if(j>0) dp[i][j]=max(dp[i][j],dp[i-1][j-1]+card[i].c);//选C
if(i-j<=b) dp[i][j]=max(dp[i][j],dp[i-1][j]+card[i].b);//b还能继续更新
else dp[i][j]=max(dp[i][j],dp[i-1][j]+card[i].a);
}
}
cout<<dp[n][c]<<endl;
return 0;
}
C 题目链接:点击这里传送
思路:
模拟。该咋说咋做。
#include<bits/stdc++.h>
using namespace std;
#define ZERO 1e-6
int n, m;
const int Max_n = 100 + 10;
struct node {
int id, val;
};
node a[Max_n];
bool cmp(node x, node y){
return x.id < y.id;
}
void solve(){
cin >> n >> m;
int suma = 0;
for (int i=1;i<=n;++i){
cin >> a[i].id >> a[i].val;
suma += a[i].val;
}
double aver = 1.0*suma/n;
for (int i=1;i<=n;++i){
if (a[i].id == m){
if(a[i].val<60)
a[i].val = 60;
continue;
}
if (a[i].val - aver > ZERO){
a[i].val = max(0, a[i].val-2);
}
}
sort(a+1, a+1+n, cmp);
for (int i=1;i<=n;++i){
printf(i==n?"%d\n":"%d ", a[i].val);
}
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
solve();
return 0;
}
D 题目链接:点击这里传送
思路:
先假设不用维护排与排之间的关系,就是单纯维护两个递增序列。
定义
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]为一共i个人,第一排j个人,第二排i-j个人的组合数量
由于身高相同的人之间位置是可以相互交换的,就把所有身高相同的人缩为一个点。假设一共有x个这样的人,枚举去了第一排k个人,第二排x-k个人的所有合法情况,将这些情况的值累加起来就是答案。
现在再考虑之前忽视的条件。这个条件设置的很精妙,如果我们排序后优先把人放到第一排,这样就能够始终保证相同位置第一排的人不会比第二排的人高。
这样的话,我们得始终保证第一排的人大于等于第二排的人,即
j
>
=
i
−
j
j>=i-j
j>=i−j。将所有
j
<
i
−
j
j<i-j
j<i−j的情况视为非法情况,在本题中意味着值为0,也就不用管它。
具体实现和细节看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 5005
#define mod 998244353
ll n;
ll a[MAXN];
vector <ll> v;
ll cnt[MAXN];
ll order[MAXN];//预处理阶乘,最后要乘上顺序
ll dp[MAXN][MAXN];//dp[i][j]表示一共i个人,第一排放了j个人,第二排放了i-j个人的方案数量
void pre()
{
order[0]=1;
for(int i=1;i<=5000;i++) order[i]=order[i-1]*i%mod;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
pre();
for(int i=1;i<=n;i++)
{
cin>>a[i];
cnt[a[i]]++;
}
sort(a+1,a+1+n);//排序后每次都先往第一排放,再往第二排放,一直保证第一排的人数不比第二排少,这样就可以完美避开题目的限制条件(因为后加的肯定比先加的高)
for(int i=1;i<=n;i++)
{
if(cnt[i]>0) v.emplace_back(cnt[i]);
}
dp[0][0]=1;//初始状态
ll now=0;//目前已经处理过的人,相当于i
for(int i=0;i<v.size();i++)
{
ll x=v[i];//身高这么高的有x个人,缩点
now+=x;
for(int j=min(now,n/2);j>=now-j;j--)//第一排的人数,取min是因为j不能超过一半,j>=now-j就能保证第一排的人不比第二排的人少
{
for(int k=0;k<=x&&k<=j;k++)//分配给了第一排k个人
{
dp[now][j]=(dp[now][j]+dp[now-x][j-k])%mod;//状态转移方程,累加上一次第一排人数为j-k的情况
}
}
}
ll ans=dp[n][n/2];//这个结果只是组合的情况种数,由于身高相同的人可以随意站位,还要乘上顺序(阶乘)
for(int i=1;i<=n;i++)
{
ans=(ans*order[cnt[i]])%mod;
}
cout<<ans<<endl;
return 0;
}
E 题目链接:点击这里传送
思路:
算期望。根据输出可以得到概率,根据题意可以得到价值,相乘就完事了。
#include<bits/stdc++.h>
using namespace std;
double c[10]={0,-7,1,31,57,9977};//贡献
#define MAXN 105
char a[MAXN];
double b[MAXN];//概率
double p[MAXN];
double n,k;
double sum;
int main()
{
//ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>a[i]>>p[i];
if(a[i]=='D') b[1]+=p[i];
else if(a[i]=='C') b[2]+=p[i];
else if(a[i]=='B') b[3]+=p[i];
else if(a[i]=='A') b[4]+=p[i];
else if(a[i]=='S') b[5]+=p[i];
}
for(int i=1;i<=5;i++)
{
sum=sum+b[i]*1.0*c[i]*1.0;
}
printf("%.4lf\n",sum*k);
return 0;
}
G 题目链接:点击这里传送
思路:
预处理出一个前缀积,一个后缀积,相乘即可,记得取模。
#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define ll long long
#define mod 998244353
ll a[MAXN];
ll b[MAXN];ll c[MAXN];
ll n;
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
b[0]=a[0];c[n-1]=a[n-1];
for(int i=1;i<n;i++) b[i]=b[i-1]*a[i]%mod;
for(int i=n-2;i>=0;i--) c[i]=c[i+1]*a[i]%mod;
cout<<c[1]<<" ";
if(n!=2)
{
for(int i=1;i<=n-2;i++)
{
cout<<b[i-1]*c[i+1]%mod<<" ";
}
}
cout<<b[n-2];
return 0;
}
H 题目链接:点击这里传送
思路:
我们可以对车祸发生的时间进行二分答案。
车祸发生有两种情况
- 车祸正在发生 即:有两辆不同类型的车在当前时间位于同一位置上
- 车祸已经发生 即:当前这辆车已经被后面的车赶上或者当前的车已经赶上了前面的车。
第一种类型非常好判断。
对于第二种类型,我们可以开两个数组
L
[
1
e
5
+
5
]
和
R
[
1
e
5
+
5
]
L[1e5+5]和R[1e5+5]
L[1e5+5]和R[1e5+5]来记录初始状态各个车辆从左往右和从右往左的排列(如果类型相同且相邻,则定义为一类,因为他们相撞没事)。根据这两个数组我们就可以找到所有车辆一开始的相对位置。当某个时间段相对位置发生改变时,就说明已经发生了车祸。
具体实现和细节参考代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 100005
ll t,n,k,p,v,type;
struct node
{
ll type;
ll speed;
ll pos;
}a[MAXN];
bool cmp(node a,node b){return a.pos<b.pos;}
ll L[MAXN],R[MAXN];//用于记录相对位置,如果前后两辆车类型相同则缩为一个点
struct c
{
ll now_pos;//过了x秒后的位置
ll index;//用于识别x秒后这是哪辆车
}car[MAXN];//存放x秒后车辆的状态
bool cmpp(c a,c b) {return a.now_pos<b.now_pos;}
bool check(ll x)//过了x秒后,查看当前状态有没有翻车
{
for(int i=1;i<=n;i++)
{
car[i].now_pos=a[i].pos+a[i].speed*x;
car[i].index=i;
}
sort(car+1,car+1+n,cmpp);
for(int i=1;i<=n;i++)
{
if(i!=1&&car[i].now_pos==car[i-1].now_pos&&a[car[i].index].type!=a[car[i-1].index].type)//两辆车在同一位置且不是同一种类型
{
return false;
}
if(i<L[car[i].index]||i>R[car[i].index]) return false;//已经被前面的车追上或者追上了后面的车(早就车祸了)
}
return true;
}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>k;//车辆的数量和车辆的种类
for(int i=1;i<=n;i++) cin>>a[i].pos>>a[i].speed>>a[i].type;
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)//记录相对位置
{
if(a[i].type==a[i-1].type) L[i]=L[i-1];
else L[i]=i;
}
for(int i=n;i>0;i--)//记录相对位置
{
if(a[i].type==a[i+1].type) R[i]=R[i+1];
else R[i]=i;
}
ll l=0;ll r=2e9+100;ll ans=-1;
while(l<=r)//对发生车祸的具体时间进行二分答案
{
ll mid=(l+r)/2;
if(check(mid))//在这个时间段还未发生过车祸
{
l=mid+1;
}
else
{
r=mid-1;
ans=mid-1;
}
}
cout<<ans<<endl;
return 0;
}
J 题目链接:点击这里传送
题意:
不考虑正负数。Alice和Bob的最佳策略就是每次拿走一个当前最大的卡片。
加入负数的话就套个绝对值,也一样。
根据正数和负数绝对值的和的大小决定先拿负数还是先拿正数。然后排序一下直接顺序拿走判断一下奇偶就行了。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 5005
int a[5005];
ll z,f,sum1,sum2;//正数,负数,Alice,Bob
ll n;
bool cmp(int a,int b){return a>b;}
int main()
{
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
if(a[i]>0) z+=a[i];
else if(a[i]<0) f+=a[i];
}
if(z>-f)
{
sort(a+1,a+1+n,cmp);
}
else sort(a+1,a+1+n);
for(int i=1;i<=n;i++)
{
if(i&1==1) sum1+=a[i];
else sum2+=a[i];
}
if(z>-f) cout<<sum1-sum2<<endl;
else cout<<sum2-sum1<<endl;
return 0;
}
K 题目链接:点击这里传送
思路:
很容易看出这是一道组合游戏的并。
将所有子游戏的sg值算出来,求出他们的异或和。如果异或和为0,后手胜;否则先手胜。这是组合游戏的并的定理。
x节点的SG值是去除x节点的后继值的SG值后最小的非负整数。由于这题没有规则,任意子串都是大串的后继,就把所有子串的sg值都求出来。
通过记忆化搜索的方式求sg值可以避免大量重复的计算。
在本题中很明显最终答案只和字符数量有关,与字符具体是啥无关。因此我们定义一个字符串并非看26个字母出现了几次,而是看各个字母出现个数的组合。两者是包含的关系,所以可以将多种情况合而为一加快程序运行时间。如aabb和ccdd是同一种情况。
具体实现方式和细节参考代码:
#include<bits/stdc++.h>
using namespace std;
//求出每种可能出现的字符串类型,把他们的sg值异或一下得到答案。大字符串可以分解为若干小字符串,这就是组合游戏的并。
//sg值定义:x节点的SG值是去除x节点的后继值的SG值后最小的非负整数。由于这题没有规则,任意子串都是大串的后继,就把所有子串的sg值都求出来
//通过记忆化搜索的方式求子串的sg值,避免重复计算
#define ll long long
#define MAXN 45
int t,n;
string s;
map<vector<int>,ll> remember;//记忆化搜索用,记录已经运算过的sg值
ll sg(vector<int> now)
{
if(remember.count(now))//他妈的sg值可以为0,给老子卡了半天
{
return remember[now];
}
//取走一个字母的情况,并求出这些新字符串的sg值
set <ll> check;//记录now的后继节点(子串)的sg值,sg值貌似挺大的,数组开不了
for(int i=0;i<now.size();i++)
{
if(now[i]==0) continue;//这个字符未出现在字符串中
now[i]--;
vector <int> temp;
for(int j=0;j<now.size();j++) temp.emplace_back(now[j]);//移走了一个字符,生成了一种子串
sort(temp.begin(),temp.end());//见64行注释
check.insert(sg(temp));//求出这个子串的sg值
//在移走一个字符的情况下,移走另一个不同的字符,并且记录下这个子串的sg值
for(int j=i+1;j<now.size();j++)
{
if(now[j]==0) continue;
now[j]--;
vector <int> tmp;
for(int k=0;k<now.size();k++) tmp.emplace_back(now[k]);
sort(tmp.begin(),tmp.end());//见64行注释
check.insert(sg(tmp));
now[j]++;//删完了还原
}
now[i]++;//删完了还原
}
//x节点的SG值是去除x节点的后继值的SG值后最小的非负整数 上面已经求完了x所有后继值的SG值 现在只需找出这个非负整数并输出即可
for(ll i=0;;i++)
{
if(!check.count(i)) return remember[now]=i;
}
}
int main()
{
std::iostream::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>t;
while(t--)
{
int xor_sg=0;//sg值的异或和
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>s;
vector <int> cnt(26,0);//记录字母出现的个数
for(int i=0;i<s.length();i++)
{
cnt[s[i]-'a']++;
}
sort(cnt.begin(),cnt.end());//点睛之笔,本题只和字符出现的个数有关,这样可以将多种情况缩为一点,如aabb和ccdd
xor_sg=xor_sg^(sg(cnt));
}
if(xor_sg==0) cout<<"Bob"<<endl;
else cout<<"Alice"<<endl;
}
return 0;
}