一、0-1规划的定义
百度百科的解释:0-1规划是决策变量仅取值0或1的一类特殊的整数规划。在处理经济管理中某些规划问题时,若决策变量采用 0-1变量即逻辑变量,可把本来需要分别各种情况加以讨论的问题统一在一个问题中讨论。
如上面所说,0-1规划是采样0和1作为逻辑变量处理问题的方法,并且经常是用在处理经济管理中。那么为题就来了,我是怎么发现这个东西的呢?学校里压根没教过这个算法。
这还得归功于B站两位UP主M_Studio和CellStudio,当然并没有直接关系,而是间接关系。
二、发现的缘由
在去年,也就是我大二上学期的时候,在B站看了M_Studio老师的2D横板闯关游戏案例教学和CellStudio老师的FPS游戏学习案例,因为当时想练一练C#编写游戏脚本,于是就学了CellStudio老师的FPS案例和M_Studio老师的小狐狸的2D横板闯关游戏案例(当然这个小狐狸系列的视频已经被下架删除了)。
当然重点都不是这些,而是在脚本中有两个防止角色二段跳的代码吸引了我。如下图
我们就算不知道Unity中的一些函数,也可以脑补出实现跳跃的方法。只要我们学过条件判断if,可以很自然的想到,if (按下空格) {角色起跳}; 这么一个逻辑伪码。但是它太简单了,以至于如果玩家一直按空格角色会一直跳,这显然不符合市面上大部分的游戏规则。
于是我们看上图这段代码,在这个横板2D闯关游戏案例里,他用了一个coll.isTouchingLayers(ground)来检测角色是否在地面上。在FPS案例里,同样有characterController.isGrounded来检测角色是否在地面上。
这样我们就可以想到,角色起跳时要同时满足两个条件,一个是玩家按下跳跃键,二是角色在地面上,至于这两个函数是怎么检测角色在地面上的咱们不说。我想说的是,角色是否在地面上这个是否,它让你想到了什么?Bool型对吗?0/1,True or False。这就是我想说的,完全可以当成如果角色在地面上,那么这个isGrounded变量就是1,不在地面上就是0,这是一个很简单的逻辑,却能解决很多问题。
三、实际应用
1.折返约瑟夫问题
我并不是发现了这个方法之后马上应用的,而是在大二上学期的一次数据结构作业中,一次偶然的机会发现了这个方法的好用之处。那次作业我也发了文章,详见:[C语言、C++]数据结构作业:用双向链表和0-1变量实现折返约瑟夫问题_反约瑟夫问题
对,折返约瑟夫问题,我们回忆一下普通的约瑟夫问题,是约瑟夫环,用循环链表就可以解决。然后那次作业留的是修改后的折返约瑟夫问题,也就是摒弃了环的结构,而变成了线性,从一端到另一端,然后再往回遍历,循环往复,那我们自然想到双向链表,然后让指针在两头来回遍历即可,这里有一个问题,如何让程序知道,你的指针此时是应该向右遍历还是向左遍历呢?这是一个嵌套的过程,有人可能会说用递归,我没试过但感觉可以,但是很难想很费脑子。于是我们之上介绍的isGrounded变量就起作用了,只不过这里我换成了isBack,没有区别,反正都是0/1。
#include <iostream>
using namespace std;
typedef struct LinkNode {
int data;
LinkNode* next;
LinkNode* pre;
}*LinkList;
void InitLinkList(LinkList& L) {
L = NULL;
}
void CreatRear(LinkList& L,int n) {
L = NULL;
if (n <= 0)return;
else {
L = new LinkNode;
cin >> L->data;
L->pre = NULL; L->next = NULL;
if (n > 1) {
LinkNode* r = L;
for (int i = 1; i < n; i++) {
LinkNode* s = new LinkNode;
cin >> s->data;
r->next = s;
s->pre = r;
r = r->next;
}
r->next = NULL;
}
}
}
void Returning_Joseph(int n, int m) {
LinkList L; InitLinkList(L);
CreatRear(L, n);
LinkNode* p = L; int isBack = 0;
for (int i = 0; i < n - 1; i++) {
int j = 0;
while (j < m - 1) {
if (isBack == 0) {
while (p->next != NULL && j < m - 1) {
p = p->next; j++;
}
if (p->next == NULL)isBack = 1;
}
if (isBack == 1) {
while (p->pre != NULL && j < m - 1) {
p = p->pre; j++;
}
if (p->pre == NULL)isBack = 0;
}
}
cout << p->data << " ";
LinkNode* r = p;
if (p->next == NULL || p->pre == NULL) {
if (p->next == NULL) {
p = p->pre;
delete(p->next);
}
else {
p = p->next;
delete(p->pre);
}
}
else {
p->pre->next = p->next;
p->next->pre = p->pre;
if (isBack == 0)p = p->next;
else p = p->pre;
r->next = NULL; r->pre = NULL;
delete(r);
}
}
cout << endl;
cout << p->data << "是胜者";
}
int main(){
Returning_Joseph(5, 3);
}
0/1的逻辑也很简单,只要发现下一个后继/前驱指针为NULL,改变isBack的值,前面写两个分支条件判断,isBack是0,向有遍历指针,isBack是1,向左遍历指针,问题就解决了。
2.OpenGL机器人走路摆臂循环
这是最近计算机图形学OpenGL的一次作业,让写一个机器人的程序,其中有一个要求,就是当按下‘w’键时,机器人会摆臂迈腿,像人一样,于是我就又想到了这个方法。
void keyboard(unsigned char key, int x, int y)
{
switch (key) {
case'f':
feet1 = (feet1 + 5) % 90;//180
feet2 = (feet2 + 5) % 90;
glutPostRedisplay();
break;
case 's':
arm2 = (arm2 + 5) % 360;
glutPostRedisplay();
break;
case 'e':
hand3 = (hand3 - 5) % 180;//180
glutPostRedisplay();
break;
case 'h':
hand2 = (hand2 - 5) % 180;
hand4 = (hand4 - 5) % 180;
glutPostRedisplay();
break;
case 'w':
if (isnext == 0) {
arm2 = (arm2 + 5) % 360;
arm1 = (arm1 - 5) % 360;
leg1 = (leg1 - 5) % 180;
leg2 = (leg2 + 5) % 180;
if (leg2 == 40) {
isnext = 1;
}
}
else {
arm2 = (arm2 - 5) % 360;
arm1 = (arm1 + 5) % 360;
leg1 = (leg1 + 5) % 180;
leg2 = (leg2 - 5) % 180;
if (leg2 == -40) {
isnext = 0;
}
}
glutPostRedisplay();
break;
case 27:
//exit(0);
break;
default:
break;
}
}
如上是键盘交互的代码,咱们不看那么多直接看有关w的那段,如下面:
case 'w':
if (isnext == 0) {
arm2 = (arm2 + 5) % 360;
arm1 = (arm1 - 5) % 360;
leg1 = (leg1 - 5) % 180;
leg2 = (leg2 + 5) % 180;
if (leg2 == 40) {
isnext = 1;
}
}
else {
arm2 = (arm2 - 5) % 360;
arm1 = (arm1 + 5) % 360;
leg1 = (leg1 + 5) % 180;
leg2 = (leg2 - 5) % 180;
if (leg2 == -40) {
isnext = 0;
}
}
通过上面的代码,大家应该知道了大致的思路,isnext就是我设的一个0/1的逻辑变量,当它为0时,左胳膊和右腿往前旋转,右胳膊和左腿往后旋转,当isnext为1的时候,再反过来就好了。改变isnext的条件也很简单,就是腿旋转达到一定角度的时候,比如左腿向前旋转到40°的时候更改,这里其实我简化了,因为胳膊和腿的旋转步长是相同的,所以判断胳膊和腿的任何一个旋转到一定角度然后更改isnext变量都可以,否则会更加复杂,但是我说的这个0/1变量的思想已经包含在其中了。
四、视频讲解
0-1规划在编程问题中的应用(UnityC#脚本/折返约瑟夫/OpenGL机器人摆臂循环)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1UM4y1b7pK/?spm_id_from=333.999.0.0
五、总结
总之,虽然这不是一个什么算法,但是我觉得这是解决一套问题的一种非常好用的方法,简单的说,只要你解决问题的过程中需要分情况讨论,且两种情况是相互约束的这么一种逻辑,就比如地面检测,你不可能同时在地上和空中两种状态,折返约瑟夫你不可能同时向左和右遍历,机器人走路也是,不可能同时迈两只腿或者抬起两只胳膊,这些都是一种逻辑,也就都可以用这种方法解决,虽然很简单,但是能解决很多问题,在最开始的时候我还不知道这个叫做0-1规划,我自己乱起的叫0-1变量,但是对我来说还是很有意义的,学习中的发现的快乐可能就在于此吧。