ACM-ICPC 2018 南京赛区网络预赛

ACM-ICPC 2018 南京赛区网络预赛

  • 全程被学长带飞

A. An Olympian Math Problem


题目描述

S(n) = 1 1 ! + 2 2! + 3 3! + … + (n-1) (n-1)!
给定n,求S(n) % n

分析

高中数学题。
观察式子可得,k * k! = (k+1)! - (k)!
所以S(n) = 2! - 1! + 3! - 2! + … + n! - (n-1)! = (n! - 1) % n = n-1
所以输出n-1即可

代码

#include<iostream>
using namespace std;
typedef long long LL;
int main()
{
  int kase;
  LL n;
  cin >> kase;
  while(kase--)
  {
    cin >> n;
    cout << n-1 << endl;
  }
}

E. AC Challenge


题目描述

有n道题(n <= 20),每个题目都拥有一个分数a和一个分数b。每道题都有s个它的前置题目,你必须把前置题目做完才能做这道题。
每分钟可以做一题,在第t分钟做的题获得的分数为a * t + b
每道题都可以选择做或者不做,求一种做题方案,使得获得的分数最高

分析

个人感觉属于很基础的状压DP,我之前都没怎么碰过这类题。
由于每道题的a和b可能是负数,很显然如果a和b都是负分的话这题就不用做了。换句话说,最差得分也是0分,不可能是负分。
考虑到n的范围非常小,我们可以从n入手。考虑状压dp,我们将每道题的做题情况看作二进制下的0和1。比如00110110就代表做了2,3,5,6这四道题的状态。
枚举每一个状态,既然我们已经知道了当前状态的做题情况,我们也就知道了当前状态经过的分钟数t。对于每一个状态,它都是通过一个前置状态多做一题而转移的,比如011010这个状态可以通过011000,010010,001010这三个状态转移而来。易得转移方程dp[k] = max(dp[i]) i为前置状态
在考虑一个状态的前置状态时, 要注意是否满足做这道题的条件。

代码

//状压DP
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int MAXN = (1<<20) + 10;
const int maxn = 30;
int ans,n,dp[MAXN];
struct Pro  //记录每个题目的a,b与前置题目要求
{
  int a,b,s;
  LL state;
}pro[maxn];

int get_time(int s)  //获得一个状态的时间
{
  int t = 0;
  while(s > 0)
  {
    if(s&1) t++;
    s >>= 1;
  }
  return t;
}

void input(int k)
{
  scanf("%d%d%d",&pro[k].a,&pro[k].b,&pro[k].s);
  if(pro[k].s > 0)
  {
    int p;
    for(int i=0;i<pro[k].s;i++)
    {
      scanf("%d",&p);
      pro[k].state |= (1 << (p-1));
    }
  }
}

void solve()
{
  ans = 0;
  dp[0] = 0;
  for(int i=1;i<(1<<n);i++)  //枚举每一种状态
  {
    int tt = get_time(i);
    //cout << tt << endl;
    for(int j=0;j<n;j++)  //枚举每个状态的前置状态
    {
      if(i & (1<<j))  //
      {
        int tmp = i ^ (1<<j);
        if((i & pro[j].state) == pro[j].state)  //判断是否满足条件,若满足则更新状态
        {
          dp[i] = max(dp[i],dp[tmp] + tt * pro[j].a + pro[j].b);
          //cout << dp[i] << endl;
        }
      }
      ans = max(ans,dp[i]);
    }
  }
}

int main()
{
  memset(dp,-0x3f,sizeof(dp));
  scanf("%d",&n);
  for(int i=0;i<n;i++)
    input(i);
  solve();
  printf("%d\n",ans);
}

J. Sum


题目描述

对于一个数num,我们可以把它分解成p(m) m + p(n) n + p(k) k的形式。
若一个数的所有质因数的指数都小于2,我们称之为square-free number。
比如6 = 2
3 ,6是一个square-free number
而8 = 2 2 2,所以8不是
设函数f(n),表示n可以拆分为两个square-free number a 和 b的乘积形式的方案数
比如6可以拆分为 1 6 , 6 1 , 2 3 , 3 2,所以f(6) = 4
对于给定n,求f(1) + f(2) + f(3) + … + f(n)

题目分析

这题有点需要思维..
首先可以发现,对于素数k,f(k) = 2
如果一个数k的一个质因子的指数大于2,则f(k) = 0
如果一个数k的一个质因子p的指数为1,则f(k) = 2 f(k / p) , 因为将k/p分解成a b的形式后,p可以放在a或者b上,贡献为2
如果一个数k的一个质因子p的指数为2,则f(k) = f(k/p/p) , 因为k/p/p分解后,将两个p分别放在a和b上,无贡献

暴力做的话,需要枚举每一个数的质因子,然后看每个质因子的指数,肯定要超时。
我们可以只考虑最小质因子,通过递推的方式在O(n)下解决问题。因为欧拉筛可以筛出每一个数的最小质因子,所以我们先进行一遍欧拉筛,然后通过计算最小质因子的指数来递推

代码

//欧拉筛
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 2e7;

int tot,prime[maxn+10],f[maxn+10],summ[maxn+10],min_prime[maxn+10];

void Euler()
{
  for(int i=2;i<=maxn;i++)
  {
    if(!min_prime[i])  //如果没有找到素因子,那么它本身就是素数
    {
      prime[tot++] = i;
      min_prime[i] = i;
    }
    for(int j=0;j<tot && prime[j] * i <= maxn;j++)
    {

      min_prime[prime[j] * i] = prime[j];
      if(!(i % prime[j])) break;
    }
  }
}

void solve()
{
  f[1] = 1;
  for(int i=2;i<=maxn;i++)
  {
    int p = min_prime[i];
    if(i == p) f[i] = 2;
    else
    {
      if(i % (p * p * p) == 0)  f[i] = 0;
      else if(i % (p * p) == 0) f[i] = f[i / p / p];
      else f[i] = 2 * f[i / p];
    }
  }
  summ[1] = f[1];
  for(int i=2;i<=maxn;i++)  summ[i] = summ[i-1] + f[i];
}

int main()
{
  Euler();
  //for(int i=1;i<=20;i++)  cout << min_prime[i] << endl;
  solve();
  int kase,n;
  cin >> kase;
  while(kase--)
  {
    cin >> n;
    cout << summ[n] << endl;
  }
}

L. Magical Girl Haze


题目描述

给出一个有向图,有n个节点m条边,现在你可以将最多k条边的长度变为0。求1到n的最短路

分析

看起来很复杂,实际上非常简单。
构造k层这样的图,如果节点u和v连通,那么就连通当前层的u–>v,并将当前层的u和上一层的v连通,将权值设为0
太巧妙了。
注意spfa会被卡,这道题要用dijkstra + heap

代码

//dij堆优化?
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
const int maxn = 5e6 + 10;
const int inf = 0x3f3f3f3f;
typedef long long LL;
int n,m,k,cnt,head[maxn],vis[maxn];
LL d[maxn];
struct Edge
{
  int next;
  int to;
  int dis;
}edge[maxn];

struct Node
{
  int to;
  LL dis;
  bool operator < (const Node &a) const
  {
    return dis > a.dis;
  }
};

void add_edge(int a,int b,int d)
{
  edge[cnt].next = head[a];
  edge[cnt].to = b;
  edge[cnt].dis = d;
  head[a] = cnt++;
}

void dijkstra()
{
  priority_queue<Node> pq;
  d[1] = 0;
  pq.push(Node{1,0});
  while(!pq.empty())
  {
    Node t = pq.top();pq.pop();
    vis[t.to] = 1;
    for(int i=head[t.to];i!=-1;i=edge[i].next)
    {
      int next = edge[i].to;
      LL dis = edge[i].dis;
      if(vis[next]) continue;
      if(d[next] > d[t.to] + dis)
      {
        d[next] = d[t.to] + dis;
        pq.push(Node{next,d[next]});
      }
    }
  }
}

void init()
{
  cnt = 0;
  memset(d,inf,sizeof(d));
  memset(vis,0,sizeof(vis));
  memset(head,-1,sizeof(head));
}

int main()
{
  int kase,a,b,c;
  scanf("%d",&kase);
  while(kase--)
  {
    scanf("%d%d%d",&n,&m,&k);
    init();
    for(int i=0;i<m;i++)
    {
      scanf("%d%d%d",&a,&b,&c);
      for(int j=0;j<=k;j++)
      {
        add_edge(a+j*n,b+j*n,c);
        if(j != k)  add_edge(a+j*n,b+(j+1)*n,0);
      }
    }
    dijkstra();
    LL ans = inf;
    for(int i=0;i<=k;i++) ans = min(ans,d[n+i*n]);
    printf("%lld\n",ans);
  }
  return 0;
}

这里要关注一下手撸简易链表的操作

int head[maxn];
struct Edge
{
  int next;
  int to;
  int dis;
}edge[maxn];


void add_edge(int a,int b,int d)
{
  edge[cnt].next = head[a];
  edge[cnt].to = b;
  edge[cnt].dis = d;
  head[a] = cnt++;
}

不太好描述…理解一下吧

... ... ...