仲舟の奇妙算法(二)【个人版】

11.无向图删边问题
原题:https://ac.nowcoder.com/acm/contest/11257/C
问题:有n个顶点的无向完全连通图,求删除多少个三角形后,剩余边数可以比n小,并输出所有删除结果(有多解)。(n<2000)
算法类型:new
时间复杂度:o(n2logn)
空间复杂度:o(<n2)
算法思路:如果建立邻接矩阵,从(1,2,3)暴搜也不会t,但会wa——由于从小的点开始删,删到最后,越大结点的会剩越多的不能组成三角形的边,也就是把原来能构成三角形的边全连到了一个点上,这样造成了很大的浪费,并且最后剩余的边往往大于n,所以wa了。因此,解决这题的核心思想,就是能做到边的平均分配,那么如何做到边的平均分配且不重边而且不t?这是个很难的问题,但是我们可以从小的方面解决:如果求不同边三角形不好求,那就先求不同边,那就好求了——(1,2)(1,3)……(1,100)(设n=100),然而会发现,如果这么取了的话,就会出现上面的“一点多边”问题,没有"平均分配",要平均就可以把所有对边连起来(1,50)(2,51)……(50,100)这样可以了,那找这些有什么用呢?线都是由两个点构成的,那么我再增加一个点,不就是三角形了?如果我让n+1的点,将上面的点全连起来,不就有50个平均分配的三角形?BUT!虽然100个点平均分配了(各删2条),但是新的点n+1被删了100条线,并没平均,那么可以再增加n+2,再去删1~100(此时由于不重边,可以(2,50)(3,51)……(50,1)这么删),加到n+50,这样就都“平均分配"了,但问题又来了——下一步怎么走?反一下?把n到n+50两两连,再在1到100选点连?那么比例不够呀!那就把51到n+50两两连,在1到50选点?看起来好像没问题,但——我们确定了1到n是互相连了的,n+1也把1到n所有都连了,那么51连n+50必会存在重复边!但同时,我们可以确定n+1到n+50各点是相互独立的,所以最保守的方式就是在n+1到n+50范围去连以及取点,但是,1到100只删了一半,他们还需要连线,连接他们可能又有重边,“不安全”,要是我们能解决掉1到100所有边,把范围缩小到n+1到n+50,把问题规模缩小就好了。那能缩小吗?可以的,回到前面,如果我扩建到n+50能让他们删100,那我扩建到n+100是否可以让他们全删?也就是2n,那么这题思路就出来了:给定n个顶点,将1到n/2作为选点,将n/2+1到n作为连点,连完之后所有连点连完,剩余选点再二分,重复上述操作,最后就能得出结果。这就是从建立“平均分配”思想到打破“平均分配”得出规律的过程。
算法难点:算法主要难点是思维,有了思维成功九成,剩下的就是如何再n/2+1到n的范围每次连边不同了,北大采用了i,m+(i+j-1)%(n-m-1)+1,m+(i-j+n-m-2)%(n-m-1)+1;的方法计算,可以通过debug查看选边过程。
研究时间:2021.8.2 8h

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

vector<tuple<int,int,int>> edges;
const int N=2000+5;
bool lk[N][N];
int sum[N];
void work(int n) {
    if(n<=2) {
        return ;
    }
    if(n==3) {
        edges.push_back({1,2,3});
        return ;
    }
    int m=(n+1)/2;
    if((n-m)%2)--m;
    if(m%2&&m*2-1==n)m-=2;
    for(int i=1; i<=min(m,n-m-1); ++i) {
        edges.push_back({i,m+i,n});
        for(int j=1; j<(n-m)/2; ++j)
            edges.push_back({i,m+(i+j-1)%(n-m-1)+1,m+(i-j+n-m-2)%(n-m-1)+1});
    }
    work(m);
}
/*void Color(int col,int bac) {
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),col|0|0|bac*16);
}*/
int main() {
#ifdef kcz
    freopen("1.in","r",stdin);//freopen("1.out","w",stdout);
#endif
    int n;
    cin>>n;
    for(int i=1; i<=n; i++)
        sum[i]=n-1;
    work(n);
    cout<<edges.size()<<endl;
    for(int i=1; i<=n; ++i)lk[i][i]=1;
    for(auto [x,y,z]:edges) {
        // Color(15,0);
        printf("%d %d %d\n",x,y,z);
        /*sum[x]-=2;
        sum[y]-=2;
        sum[z]-=2;
        for(int i=1;i<=n;i++)
        {
            Color(0,sum[i]%15+1);
            printf("%2d ",sum[i]);
        }
        Color(15,0);
        printf("\n");*/
        lk[x][y]=lk[y][z]=lk[x][z]=lk[y][x]=lk[z][y]=lk[z][x]=1;
    }
}

input:
在这里插入图片描述
output:
在这里插入图片描述
debug:
各个结点剩余边数
在这里插入图片描述

12.统计子序列数目问题
原题:https://leetcode-cn.com/problems/count-number-of-special-subsequences/
问题:有一数组a只有012,求a中满足含012非递减子序列个数(n<105)(mod=1e9+7)
算法类型:动态规划
时间复杂度:o(n)
空间复杂度:o(n)
算法思路:如果是求子串,用双指针,求子序列,一般是用动态规划——该题既无后效性(求前面的总数,不会用到后面的数据)以及最优子结构(前面的总数加上此数与前面的数量得出来的答案必是最优),此题可以用动态规划。那么怎么用?从题目入手,往往答案就是最后一个dp的结果,也就是dp[n][x],那x就是满足012非递减子序列个数的序列,满足012条件是前面至少01,满足01的条件是前面至少全0,也就可以设dp[i][x]是前面1~i满足x(0:全为0。1:满足01。2:满足012)条件有n个子序列,那么答案就是dp[n][2],接下来推递推公式:当a[i]=0,前面有dp[i-1][0]个全0子序列,增加这个0,相当于前面dp[i-1][0]个子序列全带上这个0,最后自己本身成为一个序列。dp[i][0]=2dp[i-1][0]+1;当a[i]=1,一样的,前面有dp[i-1][1]个01子序列,这些序列每个都带这个1又是dp[i-1][1]个,注意,他自己不能单独成一个序列,不能+1!但跟01凑完了可以跟全0凑——只要0的所有序列后面+1又是dp[i-1][0]个。dp[i][1]=2dp[i-1][1]+dp[i-1][0];当a[i]=2,同理,dp[i][2]=2dp[i-1][2]+dp[i-1][1]。这样就能得出结果了,但这样还能做的更好——我们发现递推公式i只和i-1有关,也就是说只要记录上一个数据不断刷新就行了,这样能省大量的空间!
算法难点:只要两层dp就够,那么如何做到算一次存一层,再算一次存另一层?a[i%2]可以解决,但是写起来好麻烦,不如每算一次拷贝一次,拷贝就用move()函数,比如把b[]拷贝到a[]就是b=move(a)(其中a[]和b[]是动态数组),就可以用一个数组操作,操作完后拷贝了,时间也快(用move()是改变其地址,效率o(1))。
研究时间:2021.8.5 2h

【代码因过于简单就略了】

13.明显用动态规划但超时问题
原题:https://leetcode-cn.com/problems/maximum-number-of-points-with-cost/
问题:m ∗ * n的矩阵中每一行选一个数,数的总和是得分,但每选一次都要扣abs(c[i]-c[i-1])分,也就是列数偏移量,求最高最后得分。(m,n<105&&mn<105)
算法类型:优化动态规划(动态规划+前后缀和)
时间复杂度:o(mn)
空间复杂度:o(mn)
算法思路:最优子结构和无后效性,傻子都看得出来用动态规划:dp[i][j]=max(dp[i-1][j’]+abs(j-j’))+a[i][j]。其中max(dp[i-1][j’]+abs(j-j’))就是去上一层遍历一遍找到最大值,我交了一发,t了,回头想一下,这他喵时间是o(mn2)啊!不t就有鬼了。但这题怎么想都只能用动态规划啊,那只能优化动态规划了。众所周知,动态规划至少o(mn),能优话的就是遍历上一层的最大值了:算每个值都要遍历一次,能否只遍历一次上层就把所有最大值找出来呢?从公式入手——如果把max(dp[i-1][j’]+abs(j-j’))里的j提出来,max就只跟j’有关了(跟j无关的好处是在i层dp任何j都可以通用该max),公式就变成dp[i][j]=max(dp[i-1][j’]+j’)+a[i][j]-j(j’<j)和dp[i][j]=max(dp[i-1][j’]-j’)+a[i][j]+j(j’>j),也就是说dp[i][j]=max(上层[0,j]最大-j,上层[j,n]最大+j)+a[i][j]。若要o(1)遍历求最大值,那就遍历一遍求[1,j]左最大存在l[],再反向遍历[j,n]右最大存在r[],l[i]就表示从[1,i]最大数,r[i]同理,那么公式就成了dp[i][j]=max(l[j]-j,r[j]+j)+a[i][j]。o(2mn),AC!
算法难点:思路有了,但还有不足之处——因为可能存在负数,所以每次计算要将l[],r[]重置-inf,这又多出了o(2n)的计算量,优化后其实可以将一组数据直接存在dp中,另一组和dp取max就行了。边算边取,这样又省时间又省空间。
研究时间:2021.8.6 3h

//力扣提交代码
class Solution {
public:
    long long maxPoints(vector<vector<int>>& points) {
        int m = points.size(),n = points[0].size();//m为行,n为列
        vector<long long> last(n);
        for (int i = 0; i < m; ++i) {
            vector<long long>now(n);
            long long presummax = LLONG_MIN;//LLONG_MIN是-inf
            for (int j = 0; j < n; ++j) {
                presummax = max(presummax, last[j] + j);
                now[j] = max(now[j], presummax + points[i][j] - j);
            }
            long long postsummax=LLONG_MIN;
            for (int j = n - 1; j >= 0; --j) {
                postsummax = max(postsummax, last[j] - j);
                now[j] = max(now[j], postsummax + points[i][j] + j);
            }
            last = move(now);//拷贝(会自动清除now内存,需要重新int)
        }
        return *max_element(last.begin(), last.end());//max_element()返回数组中最大数的迭代器
    }
};

14.周期坐标点问题(待处理)
原题:https://ac.nowcoder.com/acm/contest/11257/H
问题:在直角坐标系有n个不同大小的矩形,有一只每次只能跳d距离的兔子,求出兔子位于某点(x0+0.5,y0+0.5)时无论怎么跳、跳多少次都不会跳到矩形内,如果没有结果输出“NO”。(n,d<105,x,y<109)
算法类型:矩形面积并+线段树
时间复杂度:
空间复杂度:
算法思路:“无论怎么跳、跳多少次”可以发现如果给定一个点,这些点都是周期分布在图上的,如果暴力的话要把地图上的所有点判断能否走,这肯定会t,那么求一个点会不会好一点呢?——把所有的矩形通过压缩的方式(如下图)压成一个跳的范围距离,如果将所有矩形覆盖完后还存在某处没填,那么这处的所有周期点都没被覆盖。
算法难点:
研究时间:2021.8.8 5h

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

const int N=100005;
vector<pair<int,int> > op0[N],op1[N];
int n,D,t[N*4],tg[N*4];
void change(int k,int l,int r,int x,int y,int v){
	if (x<=l&&r<=y){//如果存的y头和y尾包含[x,y]
		t[k]+=v; tg[k]+=v; return;
	}
	int mid=(l+r)/2;
	if (x<=mid) change(k*2,l,mid,x,y,v);
	if (y>mid) change(k*2+1,mid+1,r,x,y,v);
	t[k]=min(t[k*2],t[k*2+1])+tg[k];
}
int ask(int k,int l,int r){
	if (l==r) return l;
	int mid=(l+r)/2;
	if (tg[k]+t[k*2]==t[k])
		return ask(k*2,l,mid);
	return ask(k*2+1,mid+1,r);
}
int main(){
	scanf("%d%d",&n,&D);
	int del=(1<<30)/D*D;
	for (int i=1;i<=n;i++){
		int x1,y1,x2,y2;
		scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
		x1+=del; y1+=del; x2+=del; y2+=del;
		vector<pair<int,int> > range1,range2;//range1存压缩图x头和尾,range2存压缩图y头和y尾
		if (x2-x1>=D) range1.push_back(pair<int,int>(0,D-1));
		else if ((x2-1)%D>=x1%D)  range1.push_back(pair<int,int>(x1%D,(x2-1)%D));
		else{
			range1.push_back(pair<int,int>(x1%D,D-1));
			range1.push_back(pair<int,int>(0,(x2-1)%D));
		}
		if (y2-y1>=D) range2.push_back(pair<int,int>(0,D-1));
		else if ((y2-1)%D>=y1%D)  range2.push_back(pair<int,int>(y1%D,(y2-1)%D));
		else{
			range2.push_back(pair<int,int>(y1%D,D-1));
			range2.push_back(pair<int,int>(0,(y2-1)%D));
		}
		for (auto j:range1)
			for (auto k:range2){
				op0[j.first].push_back(k);//op0存[x头]y头和y尾
				op1[j.second+1].push_back(k);//op1存[x尾]y头和y尾
			}
	}
	for (int i=0;i<D;i++){
		for (auto j:op0[i])
			change(1,0,D-1,j.first,j.second,1);
		for (auto j:op1[i])
			change(1,0,D-1,j.first,j.second,-1);
		//cout<<t[1]<<endl;
		if (t[1]==0){
			puts("YES");
			printf("%d %d\n",i,ask(1,0,D-1));
			return 0;
		}
	}
	puts("NO");
	
}

15.按位与与按位或问题
原题:https://ac.nowcoder.com/acm/contest/11259/D
问题:给两数组b[2,n],c[2,n],求一个数组a[1,n],使得a[i]|a[i+1]=b[i+1],a[i]+a[i+1]=c[i+1]。(n<105,ai<230,bi<231)
算法类型:位运算
时间复杂度:o(nlogn)
空间复杂度:o(3n)
算法思路:从题目数据来看,只要a中固定了一个数,其他都可以算出来,但如果通过枚举a[1]来算的话就o(n2),会t,所以不能枚举。我们再看数据范围,230、231明显是位运算,|也是,但是c他是+呀,有没有办法把他变成位运算?很难想——c[i+1]=a[i]+a[i+1]=a[i]|a[i+1]+a[i]&a[i+1]=b[i+1]+a[i]&a[i+1];(记住结论就行)。也就是可以设一个d[],使d[i+1]=c[i+1]-b[i+1]=a[i]&a[i+1];这样就成了一个位运算的题——两个数每一位之间或与与互不干扰,这样就可以通过或与与算出连锁下每个a的其中相同一位的结果,从而算出此位的a[]有多少种,最后再将每个a的所有位的结果相乘就是答案。
算法难点:计算a的一位有多少种简单,但有多少种这样的a[]是头疼的,我们可以从一组b和d开始考虑,如果b=0,d=0,那a[-1]与a[]都是0,如果前面的a[-1]结果是0,那就能继续判断,如果是1,那就无结果,如果b=0,d=1,还要分两种情况,即如果前面的是0后面就1,前1后就0,也就是说是否能合法要看前面的一位,这不经想起了dp——我们不需要把所有a列出来,就设一个bit0,bit1代表上一位是0或1存在多少种合法情况,如下表,如果b=0,d=0,那么现在的nowbit0=bit0,如果b=0,d=1则nowbit0=bit1,nowbit1=bit0,然后状态转移bit0=nowbit0,bit1=nowbit1;最后该位的答案就是bit0+bit1(由于有连锁性,即使有的bd能产生两种情况,也是取决于前面一位,所以每个位数最多情况为2种,每位答案相乘即是所求答案,一定小于230
研究时间:2021.8.11 6h

在这里插入图片描述

#include <bits/stdc++.h>
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
using namespace std;
const int N = 1e5 + 7;
ll b[N], c[N], d[N];
signed main() {
    int n;
    scanf("%d", &n);
    for(int i = 2; i <= n; ++i) scanf("%lld", b + i);
    for(int i = 2; i <= n; ++i) scanf("%lld", c + i);
    for(int i = 2; i <= n; ++i) d[i] = c[i] - b[i];
    ll ans = 1;
    for(int i = 0; i <= 31; ++i) {
        int bit0 = 1, bit1 = 1;//初始默认前面0,1均可取
        for(int j = 2; j <= n; ++j) {
            int nowbit0 = 0, nowbit1 = 0;
            int x = b[j] >> i & 1;
            int y = d[j] >> i & 1;
            if(!x && !y) nowbit0 = bit0;//b|d=0,b&d=0
            else if(x && !y) nowbit1 = bit0, nowbit0 = bit1;//b|d=1,b&d=0
            else if(x && y)  nowbit1 = bit1;//b|d=1,b&d=1
            else {bit1=bit0=0; break;}//b|d=0,b&d=1 不可能有结果,终止循环
            bit1 = nowbit1, bit0 = nowbit0;//确定当前数位状态
        }ans *= bit1 + bit0;//更新结果
        if(!ans) break;//若存在非法,直接跳出循环
    }printf("%lld\n", ans);
    return 0;
}

input&output:
在这里插入图片描述

16.位运算求满足要求的数组个数
原题:https://codeforces.com/contest/1557/problem/C
问题:有一个大小为n的数组a[],每个数都是小于2k的非负数,求满足a[1]&a[2]&a[3]&……&a[n]>=a[1]⊕a[2]⊕a[3]⊕……⊕a[n]的数组个数(1≤n≤2e5 , 0≤k≤2e5)(结果对1e9+7取模)
算法类型:数位dp+排列组合+快速幂
时间复杂度:o(k)
空间复杂度:o(1)
算法思路:根据题目所给数据可知,最大值能达到22e5,再加上是位运算题,我们就不需要知道a[]的具体值,只需要求所有a[]的每一个二进制位组成的01数组有多少组满足题意,由于枚举,每个二进制位满足题意的结果一样,那么所有的结果就是所有位数满足题意的结果相乘,而且每位满足题意的结果需要在o(1)的时间算出来。题目就简化成了:
①对于每一位二进制,用o(1)的时间求n个二进制数组b[]有多少种满足题意。
②对于所有位二进制,第i位的结果对第i-1位结果的影响
对于①通过题目发现,在所有&运算中,除非b[]全为1时等于1,否则等于0,而所有⊕运算等于0和等于1的结果各占一半。要满足题意,设数组种类有x=2n种,根据&的值分情况讨论:
Ⅰ当&为1时,⊕当n为偶数时为0,当n为奇数时为1。均满足条件,但一个是大于一个是等于。
Ⅱ当&为0时,满足条件的只有等于,由于1或0的结果各占一半,当n为偶数时⊕有x/2-1(去除Ⅰ)个0,当n为奇数时⊕有x/2个0。
于是我们可以令n=2代表所有偶数结果,n=3代表所有奇数结果作图(如下图),观察发现当n为偶数,“=”有y1=x/2-1种,“>”有1种,当n为奇数,“=”有y2=x/2+1种,无">“。这就用o(1)解决了一位的问题
对于②,可知如果i位是“=”,那么i-1位还需要”=“或“>“的情况出现,如果i位是“>”,那下面则可无需判断,直接有xi-1的组合。通过下图发现,当n为奇数时没有”>“的情况,也就是说只要把二进制每一位”=“的结果相乘就是答案,即ans=y2k,可o(1)算出。当n为偶数时,情况就复杂了:先考虑最高位i,如果为”>”,ans+=xi-1,如果为"=“,还需通过第i-1位判断:如果为”>“,ans+=y11xi-2,如果为”=“,还需通过第i-2位判断:如果为”>“,ans+=y12xi-3,如果为”=“,还需通过第i-3位判断:……也就是可以直接分析第一个”>“出现在哪一位,如果这个结果符合题意,上面的每层位是”=“,有y1种,下面的怎么取都可以,有x种,由于本位的“=”只有一种,所以第i位ans+=y1i-1xk-i,可o(k)算出。
算法难点:该算法灵活度极高,将数组拆分成k个二进制数组,通过每位用位运算规律计算满足题意结果,并生成奇偶分类讨论,还要使用动态规划考虑到各个位之间的联系,通过排列组合求”>"上下位的情况数,最后通过快速幂运算答案。
研究时间:2021.09.16 18h

在这里插入图片描述

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll MOD = 1e9 + 7;
ll qkpow(ll a,ll n,ll mod) {
    ll base=a,res=1;
    while(n) {
        if(n&1)res=(base*res)%mod;
        base=(base*base)%mod;
        n>>=1;
    }
    return res;
}
int main() {
    ll t;
    cin>>t;
    while(t--) {
        ll n,k,ans=0;
        cin>>n>>k;
        if(n&1)
            ans=qkpow(qkpow(2,n-1,MOD)+1,k,MOD);
        else {
            ans=qkpow(qkpow(2,n-1,MOD)-1,k,MOD);
            for(ll i=k; i>=1; i--)
                ans=(qkpow(qkpow(2,n-1,MOD)-1,k-i,MOD)*qkpow(qkpow(2,n,MOD),i-1,MOD)%MOD+ans)%MOD;
        }
        cout<<ans<<endl;
    }
    return 0;
}

17.k倍二进制诈骗问题
原题:https://codeforces.com/contest/1562/problem/C
问题:有个长度为n的01串s,你需要找出长度至少为n/2(向下取整)的不等子串s1和s2,使得s1=k ∗ * s2(k为非负整数)(2<=n<=2e4)
算法类型:new
时间复杂度:o(n)
空间复杂度:o(n)
算法思路:这个题比较阴间,光是看用例就会带偏:我们需要枚举所有符合条件的s1,再找到s2,使得s2左移任意位能变成s1则为答案,但算法复杂度是o(n4),必t,枚举也麻烦,应该想其他解决方案。假设我们找到了s2,s2左移一位变s1,即k=2,也就是s1=s2+0。这样的话如果我从n/2向右找,但凡找到0,不管s1s2是什么,都是答案;同理,k=1时也可以从n/2向左找0,即s1=0+s2,将该问题转化为了找0问题,最后如果没0(即全1串)就随意输出。
算法难点:难点主要在于将k设为特殊值来简化问题。
研究时间:2021.09.04 2h

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main() {
    ll t,n;
    cin>>t;
    while(t--) {
        string s;
        cin>>n>>s;
        int flag=0;
        for(int i=0; i<n/2+n%2&&!flag; i++)
            if(s[i]=='0') {
                cout<<i+1<<" "<<n<<" "<<i+2<<" "<<n<<endl;
                flag=1;
            }
        if(flag)
            continue;
        for(int i=n-1; i>=(n-n/2)&&!flag; i--)
            if(s[i]=='0') {
                cout<<"1"<<" "<<i+1<<" "<<"1"<<" "<<i<<endl;
                flag=1;
            }
        if(flag)
            continue;
        cout<<"1"<<" "<<n-1<<" "<<"2"<<" "<<n<<endl;
    }
    return 0;
}

18.整除运算关系dp问题
原题:https://codeforces.com/contest/1561/problem/D1
问题:有n阶台阶,从高到低编号1到n。有两种走法:设当前位置为x:1.走1到x-1步。2.走到x/k(2<=k<=x,x/k向下取整)。求从n走到1要多少步(2<=n<=4e6)
算法类型:动态规划+前缀和+筛法
时间复杂度:o(<n2)
空间复杂度:o(3n)
算法思路:这题很容易看出是dp,第一走法用前缀和不多讲,但第二走法就需要从头除到尾了,但是时间复杂度尾o(n2),t了。怎么办?既然除的会有很多重复,那我将dp[i]+=dp[i/j]中的所有i/j在前面执行的时候全部用数组保存,然后直接除了就可,BUT!
在这里插入图片描述
你没有这么多内存来存下标,而且算法复杂度并没有得到优化(依旧是(n2)。也就是说我们只是一个一个加起来都会超时!那我们是否可以像前面一样的用前缀和?使复杂度降至o(n)级别?
我们把第二走法每种结果写出来,所列出来的所有的商x均为+dp[x],可以发现,如果用前缀和的话,前面的值可能会发生变化,但可以发现每层的变化很少,每次变化也是上次+1。所以,每当我们算出一个dp[x]的值后,我们可以预先将x的所有倍数加上这个dp[x](因为y/k=x等价于y=x*k),然后为了避免重复,还要在x+1的所有倍数减去这个dp[x](由于dp是前缀和,减去相当于少了之前的某个结果)。
算法难点:这题代码理解起来相当困难,它打破了常规的静态前缀和,通过筛法删减来实现动态前缀和,最难理解的就是:如果在每步运算中去重。
研究时间:2021.09.07 10h

在这里插入图片描述

#include<bits/stdc++.h>
#define js ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
#define ll long long
using namespace std;
const ll MAXN= 4e6+700;
ll n,m,dp1[MAXN],dp2[MAXN],sum[MAXN];
int main() {
    cin>>n>>m;
    dp1[1]=dp2[1]=1;
    for(ll i=1; i<=n; i++) {
        (dp1[i]+=sum[i]+dp2[i-1])%m;
        dp2[i]=(dp2[i-1]+dp1[i])%m;
        (sum[i+1]+=sum[i])%m;
        for(ll j=2; j*i<=n; ++j)	{
            (sum[j*i]+=dp1[i])%=m;
            (sum[min(j*(i+1),n+1)]+=m-dp1[i])%=m;
        }
    }
    cout<<dp1[n]%m<<endl;
    return 0;
}

19.错位相加问题
原题:https://codeforces.com/contest/1567/problem/C
问题:有个人在算加法的时候进位时会向更高的一位进1,现在只知道他算错的结果,求有多少种原式会让他算成这个结果
算法类型:动态规划+new
时间复杂度:o(lnn)
空间复杂度:o(lnn)
算法思路:这题我一开始还在考虑每个答案有多少种加法合法,其实这题如果想到这点可以秒做:第一位进位进到第三位,第三位进位进到第五位,第五位进位进到第七位……也就是说可以把奇数位数分出来,偶数位数分出来,他们之间互不干扰,可以之间从答案中错位判断原加数是什么。
算法难点:null
研究时间:2021.09.25 3h

#include <bits/stdc++.h>
using namespace std;
int main() {
    int T, n;
    cin >> T;
    while (T--) {
        string n;
        cin >> n;
        int s1 = 0, s2 = 0;
        for (int i = 0; i < n.size(); i += 2) 
            s1 = s1 * 10 + n[i] - '0';
        for (int i = 1; i < n.size(); i += 2)
            s2 = s2 * 10 + n[i] - '0';
        if (!s2) 
            cout << s1 - 1 << endl;
        else 
            cout << 1LL * (s1 + 1) * (s2 + 1) - 2 << endl;
    }
    return 0;
}

20.矩阵中选择求和问题
原题:https://leetcode-cn.com/problems/minimize-the-difference-between-target-and-chosen-elements/
问题:给一个m ∗ * n的矩阵,每行选一个数,求其之和与目标值相差最小绝对值(1<=n,m,a[][]<=70)(1<=target<=800)
算法类型:动态规划
时间复杂度:o(m2n)
空间复杂度:o(mn)
算法思路:由于每种组合都有一个答案,组合共有mn的情况,一一列举肯定会t,我一开始想到了优化枚举——将每行排序,然后组合,当枚举这和比目标值大的时候,再往后枚举差值会越来越远,所以之间跳过此行枚举,然而他的结果依然近似于o(mn)。再看看数据,会发现它的a[][]值开的额外小,如果全部加起来最多也是4900,那我们无需知道其组合方式,只需知道求和结果就行,把所有可能结果存起来,用bool b[]存,b[i]代表求和为i是否存在,那么对于每层的数值,加上前面能产生所有的结果(从b[1]遍历到b[4900])就是新的结果,最后o(4900)遍历最小绝对值即可。
算法难点:难点在于从枚举问题转化为背包类型动态规划,大大降低了时间复杂度。
研究时间:2021.10.7 3h

class Solution {
public:
    int minimizeTheDifference(vector<vector<int>>& mat, int target) {
        int m = mat.size(), n = mat[0].size();
        int maxsum = 0;
        // 什么都不选时和为 0
        vector<int> f = {1};
        for (int i = 0; i < m; ++i) {
            int best = *max_element(mat[i].begin(), mat[i].end());
            vector<int> g(maxsum + best + 1);
            for (int x: mat[i]) {
                for (int j = x; j <= maxsum + x; ++j) {
                    g[j] |= f[j - x];
                }
            }
            f = move(g);
            maxsum += best;
        }
        int ans = INT_MAX;
        for (int i = 0; i <= maxsum; ++i) {
            if (f[i] && abs(i - target) < ans) {
                ans = abs(i - target);
            }
        }
        return ans;
    }
};

仲舟原创,未经允许禁止转载!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值