B.Prefix Code
题意:
给n个长度不超过10的数字串,问是否存在一个串是另一个串的前缀,如果不存在则输出Yes,如果存在就输出No
数据范围:n<=1e4
思路:
字典树。
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=1e4+5;
struct Node{
int a[maxm*10][10],tot;
int cnt[maxm*10];//记录node被经过次数
int is[maxm*10];//记录串结尾
void init(){
for(int i=0;i<=tot;i++){
memset(a[i],0,sizeof a[i]);
cnt[i]=is[i]=0;
}
tot=0;
}
void add(char *s){
int len=strlen(s);
int node=0;
for(int i=0;i<len;i++){
int v=s[i]-'0';
if(!a[node][v])a[node][v]=++tot;
node=a[node][v];
cnt[node]++;
}
is[node]=1;
}
int ask(){
for(int i=1;i<=tot;i++){
if(is[i]&&cnt[i]>1)return 1;
}
return 0;
}
}t;
char s[15];
signed main(){
int T;
scanf("%d",&T);
for(int cas=1;cas<=T;cas++){
t.init();
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%s",s);
t.add(s);
}
printf("Case #%d: ",cas);
if(!t.ask())puts("Yes");
else puts("No");
}
return 0;
}
D.Spanning Tree Removal
题意:
给n,表示有一个n个顶点的完全图
每次操作要从中删除一个生成树,问做多删除几次
且输出边的删除方案
数据范围:n<=1e3
思路:
因为完全图一共n(n-1)/2条边,一颗生成树n-1条边,容易想到可以删除n/2次
这题的难点主要在于如何构造删边方案。
做法:
把n个点围成一个圆,然后按Z字形删,这样删的话圆周的边每次只减少两条,一共n/2次减完
找一下每次删除的点的规律:
假设起始点为x,则第一次删的是x-(x+1),第二次删的是(x+1)-(x-2),第三次是(x-2)-(x+3)…
发现就是当前点先顺时针移动1格,然后逆时针移动两格,然后顺时针移动三格…代码里面有体现
code:
#include<bits/stdc++.h>
using namespace std;
signed main(){
int T;
scanf("%d",&T);
int cas=1;
while(T--){
int n;
scanf("%d",&n);
printf("Case #%d: %d\n",cas++,n/2);
for(int i=1;i<=n/2;i++){
int now=i;
int nt;
for(int j=1;j<=n-1;j++){
if(j&1){
nt=(now+j)%n;
}else{
nt=(now-j+n)%n;
}
if(!nt)nt=n;
printf("%d %d\n",now,nt);
now=nt;
}
}
}
return 0;
}
E.Cave Escape
题意:
大意:
给n和m表示有一个n*m的矩阵
给起点(stx,sty)和终点(edx,edy)
每个点(x,y)有权值v(x,y),
从每个点(i,j)可以走到相邻的点(x,y),如果是第一次走到,那么总得分会增加两点的权值积v(i,j)*v(x,y)
现在要从起点走到终点,问路径上最大得分是多少,注意到达终点之后可以继续走赚得分,然后再回来
数据范围:T组数据(T<=10),n,m<=1e3,0<=点权<100
思路:
因为点权非负,所以走过所有的点才能得到最大得分,因此起点和终点其实是没用的。
因为每个点只有第一次到的时候才会有得分,相邻格子之间建边,很容易看出来答案其实就是最大生成树
但是总点数1e6,边大概2e6,排序大约4e7,而且是多组数据,有点难顶,很多人都是1700ms左右卡过去的(时限2s)
正解是从数据范围入手,因为点权最大100,那么边权最大1e4,用类似桶的方法来存边,避免排序
具体做法是用vector+pair开的g(x)存边权为x的边,因为边权最大1e4,因此开1e4大小就行了,从大到小遍历,不需要排序
最大生成树就是常规做法,关键在于用桶存边减少复杂度
总结:当需要给边排序且边权较小时,可以用桶来存边避免排序
code:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxm=1e6+5;
vector<pair<int,int> >g[10000+5];
int pre[maxm];
int x[maxm];
int n,m,stx,sty,edx,edy;
int A,B,C,P;
int ffind(int x){
return pre[x]==x?x:pre[x]=ffind(pre[x]);
}
int id(int i,int j){
return (i-1)*m+j;
}
signed main(){
int T;
scanf("%d",&T);
for(int cas=1;cas<=T;cas++){
scanf("%d%d%d%d%d%d",&n,&m,&stx,&sty,&edx,&edy);
scanf("%d%d%d%d%d%d",&x[1],&x[2],&A,&B,&C,&P);
printf("Case #%d: ",cas);
for(int i=3;i<=n*m;i++){
x[i]=(A*x[i-1]+B*x[i-2]+C)%P;
}
for(int i=0;i<=10000;i++)g[i].clear();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(i+1<=n){
int a=id(i,j);
int b=id(i+1,j);
g[x[a]*x[b]].push_back({a,b});
}
if(j+1<=m){
int a=id(i,j);
int b=id(i,j+1);
g[x[a]*x[b]].push_back({a,b});
}
}
}
for(int i=1;i<=n*m;i++)pre[i]=i;
int tot=0;
ll ans=0;
for(int i=10000;i>=0;i--){
for(auto v:g[i]){
int x=ffind(v.first);
int y=ffind(v.second);
if(x!=y){
ans+=i;
pre[x]=y;
tot++;
}
}
if(tot==n*m-1)break;
}
printf("%lld\n",ans);
}
return 0;
}
F.A Simple Problem On A Tree
题意:
给一棵n个节点的树,有q次操作:
操作有4种:
1.将u,v路径上的点权改为w
2.将u,v路径上的点权加上w
3.将u,v路径上的点权乘上w
4.查询u,v路径上的点权立方和
思路:
显然树剖,线段树维护:和,平方和,立方和,乘法标记和加法标记。
H.Tree Partition
题意:
给一颗n个顶点的树,和一个整数k
每个点有点权
要求切断k-1条边,将树分成k个连通块
每个连通块的权值为连通块内的点的点权和
问最优切割下,最大连通块的权值的最小值是多少
数据范围:n<=1e5,k<=n
思路:
最大值最小化要往二分的方向想
显然最大连通块的权值满足单调性
因此二分枚举最大连通块的权值,判断能否成立即可
如何判断权值mid能否成立:
对于某个几点,先将所有子节点所在连通块全部选中,如果总和大于mid,则必须切割
给子节点连通块从大到小排序,贪心地优先切割权值大的子连通块,切割的时候记录切割次数
如果切割总次数小于等于k-1则满足条件
详见代码
code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxm=1e5+5;
int head[maxm],nt[maxm<<1],to[maxm<<1],tot;
int a[maxm];
int n,k;
int mid;
int kk;
void init(int n){
for(int i=1;i<=n;i++)head[i]=0;
tot=0;
}
void add(int x,int y){
tot++;nt[tot]=head[x];head[x]=tot;to[tot]=y;
}
bool cmp(int a,int b){
return a>b;
}
int dfs(int x,int fa){
int sum=a[x];
vector<int>temp;
for(int i=head[x];i;i=nt[i]){
int v=to[i];
if(v==fa)continue;
int t=dfs(v,x);
temp.push_back(t);
sum+=t;
}
if(sum<=mid)return sum;
sort(temp.begin(),temp.end(),cmp);
for(int v:temp){//贪心的从大到小切割
sum-=v;
kk++;
if(sum<=mid)break;
}
return sum;
}
bool check(){
kk=0;
dfs(1,-1);
return kk<=k-1;
}
signed main(){
int T;
cin>>T;
signed cas=1;
while(T--){
cin>>n>>k;
init(n);
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
}
int sum=0;
int ma=0;
for(int i=1;i<=n;i++){
cin>>a[i];
sum+=a[i];
ma=max(ma,a[i]);
}
int ans=0;
int l=ma,r=sum;
while(l<=r){
mid=(l+r)/2;
if(check())ans=mid,r=mid-1;
else l=mid+1;
}
printf("Case #%d: ",cas++);
cout<<ans<<endl;
}
return 0;
}
K.Color Graph
题意:
给一个n个点m条边的简单图,简单图的定义是无向图,没有自环和重边。
现在你要选出来一些边,满足选出来的边不构成奇环。
问最多可以选择多少条边。
数据范围:n<=16,m<=n*(n-1)/2
思路:
无奇环的图是二分图,二进制枚举点集的左半部,利用左半部推出右半部,对边数取max就是答案。
因为最优解一定是二分图,所以这样枚举一定会枚举到答案。
code:
#include<bits/stdc++.h>
using namespace std;
const int maxm=20;
vector<int>g[maxm];
int mark[maxm];
signed main(){
int T;
scanf("%d",&T);
for(int cas=1;cas<=T;cas++){
int n,m;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)g[i].clear();
for(int i=1;i<=m;i++){
int a,b;
scanf("%d%d",&a,&b);
a--,b--;//把下标改成从0开始
g[a].push_back(b);
g[b].push_back(a);
}
int ans=0;
for(int i=0;i<(1<<n);i++){//二进制枚举左半部
for(int j=0;j<n;j++){//标记选中的点
mark[j]=(i>>j&1);
}
int temp=0;
for(int j=0;j<n;j++){//计算边
if(i>>j&1){
for(int v:g[j]){
if(!mark[v])temp++;//如果另一端没被标记说明另一端可以作为右半部,则边可选
}
}
}
ans=max(ans,temp);
}
printf("Case #%d: %d\n",cas,ans);
}
return 0;
}