文章目录
A. Cut the Triangle
题意:
给你三角形的三个点坐标,判断是否存在一条水平或垂直的直线可以将这个三角形分成两个三角形。
solution:
若三角形是直角三角形且直角边与坐标轴平行,那就不行,否则ok。
B. Block Towers
题意:
给你 n ( n ≤ 2 ∗ 1 0 5 ) n(n \leq 2*10^5) n(n≤2∗105)座方块塔,给出每座塔的高度 h i h_i hi。你可以执行以下操作任意次。对不同的塔 i , j i,j i,j,若 h i < h j h_i < h_j hi<hj,则可令 h j − − , h i + + h_j--,h_i ++ hj−−,hi++,求下标为1的塔最高能有多高。
solution:
显然第2到第n座塔全部用于抬高第一座塔。考虑怎样贪心最优。发现高度越高的塔越到后面越有用,高度较低的塔只在前面有用。那显然优先使用高度低的塔抬高第一座塔,之后再用高度较高的塔。第2至n座塔排个序,然后开贪即可。
C. Count Binary Strings
题意:
要求构造一个长度为 n ( n ≤ 100 ) n(n \leq 100) n(n≤100)的01串 s s s。给出如下限制:
对任意 ( i , j ) (i,j) (i,j)满足 1 ≤ i ≤ j ≤ n 1\leq i\leq j \leq n 1≤i≤j≤n,给出 a i , j a_{i,j} ai,j
若 a i . j = 1 , 则 s i , s i + 1 , s i + 2 . . . s j a_{i.j} = 1,则s_i,s_{i + 1},s_{i + 2}...s_j ai.j=1,则si,si+1,si+2...sj要一样
若 a i . j = 2 , 则 s i , s i + 1 , s i + 2 . . . s j a_{i.j} = 2,则s_i,s_{i + 1},s_{i + 2}...s_j ai.j=2,则si,si+1,si+2...sj至少存在两个不同的字符
若 a i . j = 0 , 则 s i , s i + 1 , s i + 2 . . . s j a_{i.j} = 0,则s_i,s_{i + 1},s_{i + 2}...s_j ai.j=0,则si,si+1,si+2...sj没有特殊限制条件
求总共可以构造多少种不同的01串。
solution:
一眼dp。状态方程很容易想,但是转移条件很难搞。处理转移条件的数组处理了半天没搞出来,结果一看jiangly的代码发现人家根本没处理。笑嘻。
设 f i , j f_{i,j} fi,j表示上一个 0 0 0出现在第 i i i位,上一个 1 1 1出现在第 j j j位可以构造符合条件的01串的方案数。
令 n e x = m a x ( i , j ) + 1 nex = max(i,j) + 1 nex=max(i,j)+1,表示下一位要填0或填1的位置。
若满足转移条件,则有
f n e x , j + = f i . j f_{nex,j} += f_{i.j} fnex,j+=fi.j(在nex位置填0)
f i , n e x + = f i , j f_{i,nex} += f_{i,j} fi,nex+=fi,j(在nex位置填1)
考虑如何去处理转移条件。这时候我们就要回顾dp的几个重要原则。一是转移无后效性,二是只需考虑上一阶段如何转移到当前阶段。
对于位置 n e x nex nex,给出的限制共有 a k , n e x ( 1 ≤ k ≤ n e x ) a_{k,nex}(1\leq k \leq nex) ak,nex(1≤k≤nex),以及 a n e x , k ( k > n e x ) a_{nex,k}(k > nex) anex,k(k>nex),对于后半部分,我们可以直接扔给后面的转移去判断,这不会存在后效性问题。所以我们只需考虑当前状态应满足所有 a k , n e x ( 1 ≤ k ≤ n e x ) a_{k,nex}(1\leq k \leq nex) ak,nex(1≤k≤nex)。现在只需要捋清楚当前状态与限制条件的关系。(然后我就寄了)。具体怎么判断,代码注释有解释,看代码吧。代码参考jiangly。
代码
#include<bits/stdc++.h>
using namespace std;
#define N 100 + 20
int ar[N][N];
long long f[N][N];
long long ans;
const long long mod = 998244353;
int main()
{
int n;
scanf("%d",&n);
for(int i = 1;i <= n;i ++)
for(int j = i;j <= n;j ++)
scanf("%d",&ar[i][j]);
f[0][0] = 1;
for(int i = 0;i <= n;i ++)
for(int j = 0;j <= n;j ++)
{
int nex = max(i,j) + 1;
if(nex == n + 1)//统计答案
{
ans = (ans + f[i][j]) % mod;
continue;
}
bool ok = 1;
for(int k = 1;k <= nex;k ++)//枚举所有限制条件
{
if(ar[k][nex] == 1 && j >= k)//第k位到第nex位要相同,如果nex填0,那么第k位到第nex位之间就不能有1
ok = 0;
if(ar[k][nex] == 2 && j < k)//第k位到第nex位至少有两位相同,如果nex填0,那么第k位到第nex位之间至少有一个1
ok = 0;
}
if(ok)
f[nex][j] = (f[nex][j] + f[i][j]) % mod;
//同理,nex为1的情况
ok = 1;
for(int k = 1;k <= nex;k ++)
{
if(ar[k][nex] == 1 && i >= k)
ok = 0;
if(ar[k][nex] == 2 && i < k)
ok = 0;
}
if(ok)
f[i][nex] = (f[i][nex] + f[i][j]) % mod;
}
printf("%lld\n",ans);
}
D. Playoff
题意:
有 2 n ( 1 ≤ n ≤ 18 ) 2^n(1\leq n\leq 18) 2n(1≤n≤18)支队伍进行比赛,每只队伍有一个能力值 p i p_i pi,其中 p p p数组是 2 n 2^n 2n的一个排列。每一轮比赛会在相邻的队伍进行,比如说 1 v s 2 1vs2 1vs2、 3 v s 4 3vs4 3vs4。赢的队伍晋级下一轮,继续和相邻队伍比赛,直到只剩下一支队伍。易知比赛一共回进行n轮。
给出一个长度为n的01字符串 s s s,其中 s i s_i si表示第 i i i场比赛的规则。
若 s i = = 0 s_i == 0 si==0则第 i i i场比赛,能力值较小的队伍获胜
若 s i = = 1 s_i == 1 si==1则第 i i i场比赛,能力值较大的队伍获胜
求可能在 p p p的所有排列情况下,可能获得胜利的能力值。输出所有可能获得胜利的能力值。
solution:
答案是,看样例猜结论。发现样例看起来都是连续的,直接求可能获得胜利的能力值的最小值和最大值,大胆交一发。然后a了。
考虑对能力值 t t t其获胜的条件是什么。对 s i = 0 s_i = 0 si=0,肯定要给他分配一个能力值比他高的对手,反之亦然。把不同规则的比赛拆出来单独看,假设能力值 t t t的选手存活到第 k k k轮,且前面 k k k轮有 c n t 0 cnt0 cnt0场比赛 s i = = 0 s_i==0 si==0,有 c n t 1 cnt1 cnt1场比赛 s i = = 1 s_i==1 si==1,那么至少有 2 c n t 0 − 1 2^{cnt0} - 1 2cnt0−1支队伍比他大, 2 c n t 1 − 1 2^{cnt1} - 1 2cnt1−1支队伍比自己小(假设 c n t 1 = 2 cnt1 = 2 cnt1=2,第一轮需要一个比自己小的选手,令他为 p p p,第二轮也需要一个比自己小的选手,令他为 q q q,而晋级到第二轮的选手肯定赢过一轮,也存在一个比自己小的选手 r r r,那么就有3个比自己小的选手 p , q , r p,q,r p,q,r。以此类推)。
所以,统计字符串01的个数,可能胜利的能力值区间即为 [ 2 c n t 1 , 2 n − 2 c n t 0 + 1 ] [2^{cnt1},2^n - 2^{cnt0} + 1] [2cnt1,2n−2cnt0+1]。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
string s;
int main()
{
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%d",&n);
cin>>s;
int cnt = 0;
for(int i = 0;i < s.size();i ++)
{
if(s[i] == '1')
cnt ++;
}
for(int i = (1<<(cnt));i <= (1<<n) - (1<<(n - cnt)) + 1;i ++)
printf("%d ",i);
}
E. Algebra Flash
题意:
给出 n ( n ≤ 3 ∗ 1 0 5 ) n(n\leq 3*10^5) n(n≤3∗105)个平台, m ( m ≤ 40 ) m(m\leq 40) m(m≤40)种颜色,每个平台都有一种颜色 c i ( 1 ≤ c i ≤ m ) c_i(1\leq c_i\leq m) ci(1≤ci≤m),启用第 i i i种颜色需要费用 v i ( v i ≤ 1 0 7 ) v_i(v_i\leq 10^7) vi(vi≤107)。初始时,你在第 1 1 1个平台。每次你可以往前跳 1 1 1步或往前跳 2 2 2步,并在若干次跳跃后到达终点。你必须启用平台相对应的颜色才能跳到这块平台上(包括起点和终点)。询问如何启用颜色才能在花费最小的情况下从起点跳到终点。
样例:
输入
5 3
1 3 2 3 1
1 10 100输出
11
解释
启用颜色1和颜色2,可以从平台1跳到平台3跳到平台5,花费为11,为最小花费。
solution:
由于一次只能跳一步或两步,最终的路径中不存在两个或以上连续的、没被跳到的点。即对任意相邻的两点,至少有一个点被跳到。将相邻的两个点所对应的颜色连一条边。问题转变成带点权的最小点覆盖问题,要求相邻的两种颜色(即有一条边相连)至少有一个被启用,求合法的最小价值。
发现 m m m比较小,但又没有小到可以直接状压。可以用类似双向搜索的思路进行状压。将 m m m分等为两部分 t 1 , t 2 t_1,t_2 t1,t2,分别表示前一半点的个数和后一半点的个数。
设状压方程 d [ s ] ( 0 ≤ s < 2 t 1 ) d[s](0\leq s< 2^{t_1}) d[s](0≤s<2t1),表示我要选用 s s s所代表的点,并使其合法所需要的最小代价。
1.如何判断s合法?
当前只考虑前一半点,所以对每个状态 s s s,二重循环枚举两个点都未被启用的点对,若这两点存在一条边,则说明此状态不合法。
2.如何转移?
若当前状态合法,则可以直接计算当前状态下启用点的 v a l val val和。
若当前状态不合法,则取当前状态超集中代价最小的。具体实现可以从后往前转移。
处理好 d d d数组后就可继续枚举状态 s ′ ( 0 ≤ s ′ < 2 t 2 ) s^\prime(0\leq s^\prime < 2^{t_2}) s′(0≤s′<2t2),对状态 s ′ s^\prime s′,首先它必须要合法,在它合法的情况下,我们要求得可以与它相结合的、费用最小的状态 s s s。对此,可以暴力枚举 s ′ s^\prime s′未被选用的点 i i i,然后暴力枚举前半部分的点 j j j,若 i , j i,j i,j存在一条边,说明当前状态下,点 j j j必须被选用。把所有要启用的点或起来得到最小子集 s s s,直接查询预处理好的 d d d数组即可。
#include<bits/stdc++.h>
using namespace std;
#define N 40 + 20
#define M 300000 + 200
int n,m,ans;
int vi[N],edge[N][N],ar[M];
int d[(1<<20) + 100];
const int inf = 0x3f3f3f3f;
int main()
{
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%d %d",&n,&m);
for(int i = 1;i <= n;i ++)
{
scanf("%d",&ar[i]);
ar[i] --;
}
for(int i = 0;i < m;i ++)
scanf("%d",&vi[i]);
for(int i = 2;i <= n;i ++)//相邻颜色连边
{
edge[ar[i]][ar[i - 1]] = 1;
edge[ar[i - 1]][ar[i]] = 1;
}
edge[ar[1]][ar[1]] = edge[ar[n]][ar[n]] = 1;//由于起点和终点必须被选用,所以可以将起点终点连一个自环
int t1 = m / 2,t2 = m - t1;
for(int s = (1<<t1) - 1;s >=0 ;s --)//从后往前枚举状态,用于不合法状态的转移
{
bool ok = 1;
for(int i = 0;i < t1;i ++)//判断当前状态是否合法
{
if(s & (1<<i))
continue;
for(int j = i;j < t1;j ++)
{
if(s & (1<<j))
continue;
if(edge[i][j])
ok = 0;
}
}
if(ok)//若合法,计算价值
{
for(int i = 0;i < t1;i ++)
{
if(s & (1<<i))
d[s] += vi[i];
}
}
else//若不合法,取最小价值的超集的价值
{
d[s] = inf;
for(int i = 0;i < t1;i ++)
{
if(!(s&(1<<i)))
d[s] = min(d[s],d[s | (1<<i)]);
}
}
}
ans = inf;
for(int s = 0;s < (1<<t2);s ++)//枚举后半部分状态
{
bool ok = 1;
for(int i = 0;i < t2;i ++)//判断合法
{
if(s&(1<<i))
continue;
for(int j = 0;j < t2;j ++)
{
if(s&(1<<j))
continue;
if(edge[t1 + i][t1 + j])
ok = 0;
}
}
if(ok)//若合法,计算价值
{
int cnt = 0,res = 0;
for(int i = 0;i < t2;i ++)
{
if(!(s&(1<<i)))//求当前状态下,可使得整张图合法的前半部分最小点集
{
for(int j = 0;j < t1;j ++)
if(edge[t1 + i][j])
res |= (1<<j);
}
else//计算当前状态花费
cnt += vi[t1 + i];
}
ans = min(ans,cnt + d[res]);//更新答案
}
}
printf("%d\n",ans);
}